Initial commit

This commit is contained in:
DavidCamejo 2025-11-04 21:08:40 -04:00
commit c75680d029
69 changed files with 11340 additions and 0 deletions

16
CHANGELOG.md Normal file
View File

@ -0,0 +1,16 @@
# Changelog
Todos los cambios notables de este proyecto se documentarán en este archivo.
## [3.0] - 2024-07-28
### Añadido
- Versión inicial pública del plugin.
- Creación automática de carpetas necesarias en la activación.
- Gestión modular de snippets (PHP, JS, CSS) mediante archivos.
- Validación de sintaxis antes de guardar snippets.
- Interfaz de administración para crear, editar, activar/desactivar y eliminar snippets.
- Soporte para backups automáticos y restauración sencilla.
- Compatibilidad con WordPress Multisite.
- Hooks principales para carga de snippets y administración.
---

119
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,119 @@
# Guía de Contribución para Simply Code
¡Gracias por tu interés en contribuir a Simply Code! Tu ayuda es fundamental para mejorar este plugin y su comunidad.
## Formas de contribuir
- Reportar errores o problemas
- Sugerir nuevas características
- Mejorar la documentación
- Enviar código (nuevas funciones, correcciones, mejoras)
- Compartir nuevos snippets útiles
---
## Reportar errores
Si encuentras un bug, por favor abre un "Issue" en GitHub e incluye:
1. Pasos para reproducir el problema
2. Comportamiento esperado y comportamiento actual
3. Versión de Simply Code, WordPress y PHP
4. Capturas de pantalla o videos si es posible
---
## Sugerir características
¿Tienes una idea para mejorar el plugin? Abre un "Issue" y describe:
- El problema que resuelve tu propuesta
- Cómo debería funcionar la nueva característica
- Por qué sería útil para otros usuarios
---
## Enviar código (Pull Requests)
1. Haz un fork del repositorio y clónalo en tu equipo.
2. Crea una rama para tu cambio:
```bash
git checkout -b feature/nombre-o-bugfix/descripcion
```
3. Realiza tus cambios siguiendo los [estándares de codificación de WordPress](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/).
4. Prueba tu código antes de hacer commit.
5. Escribe mensajes de commit claros y descriptivos.
6. Sincroniza tu rama con `main` antes de enviar el Pull Request.
7. Abre un Pull Request en GitHub, describe tus cambios y referencia cualquier Issue relacionado.
---
## Contribuir con nuevos snippets
¿Tienes un snippet útil de PHP, JavaScript o CSS? ¡Compártelo con la comunidad!
### ¿Qué tipo de snippets puedes enviar?
- Funciones personalizadas para WordPress
- Hooks y filtros útiles
- Fragmentos de JavaScript para mejorar la experiencia de usuario
- Estilos CSS para personalización rápida
- Soluciones a problemas comunes o utilidades generales
### ¿Cómo enviar tu snippet?
1. **Formato:**
- Envía tu snippet como un archivo independiente en la carpeta `community-snippets/` (o la que se indique en el repositorio).
- Usa la extensión adecuada: `.php`, `.js` o `.css`.
- Incluye al inicio del archivo un bloque de comentarios con:
```
/*
* Nombre: Nombre descriptivo del snippet
* Descripción: Explica brevemente qué hace el snippet y en qué casos es útil.
* Autor: Tu nombre o usuario de GitHub
* Requisitos: (opcional) Versión mínima de WordPress, plugins necesarios, etc.
*/
```
2. **Calidad y seguridad:**
- El código debe ser seguro, funcional y seguir los [estándares de codificación de WordPress](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/).
- No incluyas datos sensibles ni dependencias externas no revisadas.
3. **Pull Request:**
- Haz un fork del repositorio y crea una rama para tu snippet.
- Sube tu archivo a la carpeta `community-snippets/`.
- Abre un Pull Request describiendo la utilidad del snippet y cualquier detalle relevante.
4. **Revisión:**
- El equipo revisará tu snippet para asegurar su calidad y seguridad antes de aceptarlo.
- Si es aceptado, tu snippet será incluido en la biblioteca de Simply Code y se te dará crédito como autor/a.
#### Ejemplo de snippet enviado
```php
/*
* Nombre: Desactivar comentarios en medios
* Descripción: Evita que los usuarios puedan comentar en archivos adjuntos (medios).
* Autor: @ejemplo
*/
add_filter('comments_open', function($open, $post_id) {
$post = get_post($post_id);
if ($post->post_type === 'attachment') {
return false;
}
return $open;
}, 10, 2);
```
---
## Estándares de codificación
Por favor, adhiérete a los [estándares de codificación de WordPress](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/) para PHP, JavaScript y CSS. Esto ayuda a mantener la consistencia y legibilidad del código.
---
## Licencia
Al contribuir con código o snippets a Simply Code, aceptas que tu contribución se licenciará bajo la misma [Licencia GPL v2 o posterior](https://www.gnu.org/licenses/gpl-2.0.html) que el proyecto.
---
¡Gracias por ayudar a hacer Simply Code mejor para todos!

185
README.md Normal file
View File

@ -0,0 +1,185 @@
# Simply Code
![Simply Code Banner](assets/images/banner.jpg)
Simply Code es un plugin de WordPress minimalista y eficiente que moderniza la forma de gestionar código personalizado en tu sitio. Diseñado como una alternativa modular y profesional a `functions.php`, permite administrar snippets de código como módulos independientes. A diferencia de otras soluciones, Simply Code utiliza un sistema de archivos directo en lugar de la base de datos, lo que resulta en mejor rendimiento y mayor portabilidad.
## ¿Por qué Simply Code?
- 📂 **Alternativa moderna a functions.php**: Organiza tu código en módulos independientes
- 🔄 **Activación/desactivación instantánea**: Prueba cambios sin modificar archivos del tema
- 🚀 **Sin dependencia de base de datos**: Mejor rendimiento y facilidad de migración
- 👥 **Ideal para equipos**: Facilita la colaboración y el control de versiones
- 🔒 **Modo seguro**: Validación de sintaxis antes de guardar y ejecutar código
- 🎨 **Soporte completo**: PHP, JavaScript y CSS en cada snippet
## Instalación
1. Descarga el archivo ZIP del plugin
2. Ve a tu panel de WordPress > Plugins > Añadir nuevo
3. Haz clic en "Subir Plugin" y selecciona el archivo ZIP
4. Activa el plugin
## Estructura de archivos
```
simply-code/
├── admin/
│ ├── class-admin-page.php
│ ├── class-snippet-editor.php
│ └── views/
│ ├── snippet-editor.php
│ └── snippets-list.php
├── assets/
│ ├── css/
│ │ └── editor.css
│ ├── js/
│ │ └── editor.js
│ └── images/
│ └── banner.jpg
├── includes/
│ ├── class-snippet-manager.php
│ ├── class-syntax-checker.php
│ └── snippets-order.php
├── storage/
│ ├── snippets/
│ ├── js/
│ ├── css/
│ └── backups/
├── templates/
│ ├── class.php
│ ├── configuration.php
│ ├── function.php
│ └── hook.php
├── CHANGELOG.md
├── CONTRIBUTING.md
├── README.md
├── readme.txt
└── simply-code.php
```
## Uso
### Crear un nuevo snippet
1. Ve a "Simply Code" en el menú de WordPress
2. Haz clic en "Nuevo Snippet"
3. Completa los campos:
- Nombre del snippet
- Descripción
- Código PHP, JavaScript y/o CSS
4. Guarda el snippet
### Ejemplos de snippets
```php
// Función personalizada
function mi_funcion_util() {
// Tu código aquí
}
// Clase personalizada
class Mi_Clase_Personalizada {
public function __construct() {
// Inicialización
}
}
// Hook de WordPress
add_action('init', function() {
// Código a ejecutar
});
// Configuración personalizada
define('MI_CONSTANTE', 'valor');
```
```js
// JavaScript: Mostrar alerta en el frontend
document.addEventListener('DOMContentLoaded', function() {
alert('¡Hola desde Simply Code!');
});
```
```css
/* CSS: Cambiar color de fondo del body */
body {
background-color: #f5f5f5;
}
```
### Gestión de snippets
- **Activar/Desactivar**: Usa el interruptor en la lista de snippets
- **Ordenar**: Utiliza las flechas arriba/abajo para cambiar el orden de ejecución
- **Editar**: Modifica el código y configuración de cualquier snippet existente
- **Eliminar**: Elimina snippets que ya no necesites
## Ventajas del sistema basado en archivos
1. **Mejor rendimiento**: Sin consultas a la base de datos
2. **Mayor portabilidad**: Fácil de migrar entre instalaciones
3. **Control de versiones**: Los snippets pueden versionarse con Git
4. **Backups simplificados**: Copias de seguridad automáticas y fáciles de restaurar
5. **Depuración sencilla**: Los archivos son fáciles de inspeccionar y debuggear
## Modo seguro
El modo seguro realiza las siguientes validaciones:
- Comprueba la sintaxis PHP antes de guardar
- Valida la estructura del código
- Previene errores que podrían romper el sitio
## Características técnicas
- **Versión mínima de PHP**: 7.4
- **Versión mínima de WordPress**: 5.6
- **Licencia**: GPL v2 o posterior
- **Requiere privilegios**: `manage_options`
## Detalles técnicos recientes
- El plugin crea automáticamente las carpetas necesarias (`storage/snippets/`, `storage/js/`, `storage/css/`, `templates/`) si no existen, asegurando que el entorno esté listo desde la activación.
- El sistema de carga modular utiliza clases separadas para la gestión de snippets, validación de sintaxis y la interfaz de administración.
- Los hooks principales registrados son:
- `after_setup_theme` para cargar los snippets al inicio.
- `admin_menu` para registrar el menú de administración.
- `wp_enqueue_scripts` para cargar los assets de los snippets en el frontend.
- El almacenamiento de los snippets y recursos asociados se realiza exclusivamente en el sistema de archivos, nunca en la base de datos.
Consulta el archivo CHANGELOG.md para un historial detallado de cambios y mejoras.
## FAQ
### ¿Por qué usar Simply Code en lugar de functions.php?
Simply Code ofrece una gestión modular del código, con interfaz gráfica profesional y la capacidad de activar/desactivar snippets individualmente. Además, mantiene tu código organizado y facilita la colaboración en equipo.
### ¿Cómo migro mis snippets a otra instalación?
Simplemente copia el contenido de la carpeta `storage/` y el archivo `includes/snippets-order.php` a la nueva instalación. Al estar basado en archivos, la migración es sencilla y directa.
### ¿Se pierden los snippets al actualizar el plugin?
No. Simply Code mantiene los snippets en una ubicación separada y crea backups automáticos antes de las actualizaciones.
### ¿Puedo usar Simply Code en un entorno multisite?
Sí, Simply Code es compatible con WordPress multisite. Cada sitio puede tener sus propios snippets independientes.
## Contribuir
Las contribuciones son bienvenidas. Por favor, revisa las [guías de contribución](CONTRIBUTING.md) antes de enviar un pull request.
## Soporte
- 📝 [Documentación](docs/README.md)
- 🐛 [Reportar un problema](../../issues)
- 💡 [Sugerir una característica](../../issues/new?template=feature_request.md)
## Licencia
Simply Code está licenciado bajo la GPL v2 o posterior. Consulta el archivo [LICENSE](LICENSE) para más detalles.
## Créditos
Desarrollado por David Camejo & AI

326
admin/class-admin-page.php Normal file
View File

@ -0,0 +1,326 @@
<?php
if (!defined('ABSPATH')) exit;
class Simply_Code_Admin {
const OPTION_SAFE_MODE = 'simply_code_safe_mode';
/**
* Initialize admin hooks
*/
public static function init() {
add_action('admin_menu', [self::class, 'register_menu']);
add_action('admin_enqueue_scripts', [self::class, 'enqueue_admin_scripts']);
// CRÍTICO: Registrar handler AJAX para detección de hooks
add_action('wp_ajax_simply_code_detect_hooks', [Simply_Snippet_Editor::class, 'ajax_detect_hooks']);
}
/**
* Register admin menu and submenus
*/
public static function register_menu() {
add_menu_page(
'Simply Code',
'Simply Code',
'manage_options',
'simply-code',
[self::class, 'main_page'],
'dashicons-editor-code'
);
add_submenu_page(
'simply-code',
'Nuevo Snippet',
'Nuevo Snippet',
'manage_options',
'simply-code-new',
[Simply_Snippet_Editor::class, 'new_snippet']
);
}
/**
* Enqueue admin scripts and styles
*/
public static function enqueue_admin_scripts($hook) {
// Solo cargar en nuestras páginas
if (strpos($hook, 'simply-code') === false) {
return;
}
wp_enqueue_script(
'simply-code-editor',
plugins_url('assets/js/editor.js', SC_PATH . 'simply-code.php'),
['jquery'],
'1.0.0',
true
);
wp_enqueue_style(
'simply-code-editor',
plugins_url('assets/css/editor.css', SC_PATH . 'simply-code.php'),
[],
'1.0.0'
);
// Localizar script con ajaxurl
wp_localize_script('simply-code-editor', 'simply_code_ajax', [
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('simply_code_detect_hooks')
]);
}
/**
* Handle admin notices - VERSIÓN MEJORADA CON TRANSIENTS
*/
private static function handle_admin_notices() {
$notice = '';
// Manejar mensajes de éxito desde transients
$success_message = get_transient('simply_code_success');
if ($success_message) {
$notice .= '<div class="notice notice-success is-dismissible"><p>' . esc_html($success_message) . '</p></div>';
delete_transient('simply_code_success');
}
// Manejar mensajes de error desde transients
$error_message = get_transient('simply_code_error');
if ($error_message) {
$notice .= '<div class="notice notice-error is-dismissible"><p>' . esc_html($error_message) . '</p></div>';
delete_transient('simply_code_error');
}
// Mantener compatibilidad con parámetros GET existentes
if (isset($_GET['created']) && isset($_GET['snippet'])) {
$snippet_name = sanitize_text_field(urldecode($_GET['snippet']));
$notice .= sprintf(
'<div class="notice notice-success is-dismissible"><p>Snippet "%s" creado correctamente.</p></div>',
esc_html($snippet_name)
);
}
if (isset($_GET['updated']) && isset($_GET['snippet'])) {
$snippet_name = sanitize_text_field(urldecode($_GET['snippet']));
$notice .= sprintf(
'<div class="notice notice-success is-dismissible"><p>Snippet "%s" actualizado correctamente.</p></div>',
esc_html($snippet_name)
);
}
if (isset($_GET['deleted']) && isset($_GET['snippet'])) {
$snippet_name = sanitize_text_field(urldecode($_GET['snippet']));
$notice .= sprintf(
'<div class="notice notice-success is-dismissible"><p>Snippet "%s" eliminado correctamente.</p></div>',
esc_html($snippet_name)
);
}
if (isset($_GET['saved'])) {
$message = 'Snippet guardado correctamente.';
if (isset($_GET['snippet_name'])) {
$snippet_name = sanitize_text_field(urldecode($_GET['snippet_name']));
$message = sprintf('Snippet "%s" guardado correctamente.', esc_html($snippet_name));
}
$notice .= sprintf(
'<div class="notice notice-success is-dismissible"><p>%s</p></div>',
$message
);
}
if (isset($_GET['status_changed']) && isset($_GET['snippet']) && isset($_GET['status'])) {
$snippet_name = sanitize_text_field(urldecode($_GET['snippet']));
$status = $_GET['status'] === 'activated' ? 'activado' : 'desactivado';
$notice .= sprintf(
'<div class="notice notice-success is-dismissible"><p>Snippet "%s" %s correctamente.</p></div>',
esc_html($snippet_name),
$status
);
}
if (isset($_GET['reordered'])) {
$notice .= '<div class="notice notice-success is-dismissible"><p>Orden de snippets actualizado correctamente.</p></div>';
}
if (isset($_GET['safe_mode_updated'])) {
$notice .= '<div class="notice notice-success is-dismissible"><p>Configuración de modo seguro actualizada correctamente.</p></div>';
}
return $notice;
}
/**
* Safe redirect with proper buffer cleanup
*/
private static function safe_redirect($url) {
// Solo limpiar si hay buffers de usuario con contenido
if (ob_get_level() > 0 && ob_get_contents() !== false) {
$buffer_content = ob_get_contents();
if (!empty(trim($buffer_content))) {
@ob_end_clean();
}
}
// Verificar que no se hayan enviado headers
if (headers_sent($file, $line)) {
error_log("Simply Code: Headers already sent in {$file} at line {$line}, cannot redirect to: {$url}");
echo '<script>setTimeout(function(){ window.location.href = "' . esc_js($url) . '"; }, 100);</script>';
echo '<noscript><meta http-equiv="refresh" content="0;url=' . esc_attr($url) . '" /></noscript>';
exit;
}
// Usar wp_safe_redirect que es más robusto
$result = wp_safe_redirect($url);
if (!$result) {
error_log("Simply Code: wp_safe_redirect failed for URL: {$url}");
echo '<script>window.location.href = "' . esc_js($url) . '";</script>';
}
exit;
}
/**
* Main admin page handler
*/
public static function main_page() {
$action_message = '';
// Verificar permisos
if (!current_user_can('manage_options')) {
wp_die(__('No tienes permisos suficientes para acceder a esta página.'));
}
// CRÍTICO: Manejar POST antes de cualquier output
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (empty($_POST['_wpnonce']) || !wp_verify_nonce($_POST['_wpnonce'], 'simply_code_actions')) {
wp_die('Error de seguridad');
}
// Solo delete requiere redirect (para evitar resubmisión accidental)
if (isset($_POST['delete_snippet'])) {
self::handle_deletion(); // Hace redirect y exit
}
// Otras acciones sin redirect para evitar el efecto "doble carga"
if (isset($_POST['toggle_snippet_status'])) {
$action_message = self::handle_status_toggle_inline();
}
elseif (isset($_POST['move_up']) || isset($_POST['move_down'])) {
$action_message = self::handle_reordering_inline();
}
elseif (isset($_POST['safe_mode_toggle'])) {
$action_message = self::handle_safe_mode_inline();
}
else {
// Si no coincide con ninguna acción conocida
$action_message = 'Acción POST no reconocida';
}
}
// Mostrar notices de URL parameters
$notice = self::handle_admin_notices();
// Agregar mensaje de acción inline si existe
if ($action_message) {
$notice .= '<div class="notice notice-success is-dismissible"><p>' . esc_html($action_message) . '</p></div>';
}
// Obtener snippets
$snippets = Simply_Snippet_Manager::list_snippets(true, true);
// Mostrar notices
if ($notice) {
echo wp_kses_post($notice);
}
// Renderizar vista
include SC_PATH . 'admin/views/snippets-list.php';
}
/**
* Handle status toggle without redirect
*/
private static function handle_status_toggle_inline() {
if (!isset($_POST['toggle_snippet_status'], $_POST['snippet_name'])) {
return 'Error: Datos de formulario incompletos para cambio de estado.';
}
$snippet_name = sanitize_text_field($_POST['snippet_name']);
$new_status = isset($_POST['snippet_active']);
if (Simply_Snippet_Manager::toggle_snippet_status($snippet_name, $new_status)) {
$status_text = $new_status ? 'activado' : 'desactivado';
return sprintf('Snippet "%s" %s correctamente.', esc_html($snippet_name), $status_text);
}
return sprintf('Error al cambiar el estado del snippet "%s".', esc_html($snippet_name));
}
/**
* Handle reordering without redirect
*/
private static function handle_reordering_inline() {
if (!isset($_POST['move_up']) && !isset($_POST['move_down'])) {
return 'Error: Acción de reordenamiento no especificada.';
}
$snippets = Simply_Snippet_Manager::list_snippets(true);
$i = isset($_POST['move_up']) ? (int)$_POST['move_up'] : (int)$_POST['move_down'];
$names = array_map(function($s) { return $s['name']; }, $snippets);
$changed = false;
if (isset($_POST['move_up']) && $i > 0 && $i < count($names)) {
$tmp = $names[$i-1];
$names[$i-1] = $names[$i];
$names[$i] = $tmp;
$changed = true;
}
elseif (isset($_POST['move_down']) && $i >= 0 && $i < count($names) - 1) {
$tmp = $names[$i+1];
$names[$i+1] = $names[$i];
$names[$i] = $tmp;
$changed = true;
}
if ($changed && Simply_Snippet_Manager::update_snippets_order($names)) {
return 'Orden de snippets actualizado correctamente.';
}
return 'Error al actualizar el orden de los snippets.';
}
/**
* Handle safe mode without redirect
*/
private static function handle_safe_mode_inline() {
if (!isset($_POST['safe_mode_toggle'])) {
return 'Error: Configuración de modo seguro no especificada.';
}
update_option(
self::OPTION_SAFE_MODE,
isset($_POST['safe_mode']) && $_POST['safe_mode'] === 'on' ? 'on' : 'off'
);
return 'Configuración de modo seguro actualizada correctamente.';
}
/**
* Handle deletion with redirect
*/
private static function handle_deletion() {
if (!isset($_POST['delete_snippet'], $_POST['snippet_name'])) {
wp_die('Error: Datos de eliminación incompletos.');
}
$snippet_name = sanitize_text_field($_POST['snippet_name']);
if (Simply_Snippet_Manager::delete_snippet($snippet_name)) {
$success_message = sprintf('Snippet "%s" eliminado correctamente.', esc_html($snippet_name));
set_transient('simply_code_success', $success_message, 45);
$redirect_url = admin_url('admin.php?page=simply-code');
self::safe_redirect($redirect_url);
return; // Never reached
}
wp_die('Error al eliminar el snippet "' . esc_html($snippet_name) . '".');
}
}

View File

@ -0,0 +1,330 @@
<?php
if (!defined('ABSPATH')) exit;
class Simply_Snippet_Editor {
private static $templates = null;
/**
* Página principal del editor - maneja tanto creación como edición
*/
public static function new_snippet() {
// CRÍTICO: Manejar POST antes de cualquier output
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
self::handle_post_request();
// Si llegamos aquí, hubo un error (el éxito hace redirect y exit)
}
// Mostrar errores si los hay
self::show_stored_errors();
// Preparar y mostrar la vista
$edit_mode = isset($_GET['edit']);
$snippet_name = $edit_mode ? sanitize_file_name($_GET['edit']) : '';
$view_data = self::prepare_view_data($edit_mode, $snippet_name);
self::render_view($view_data);
}
/**
* Maneja la solicitud POST de forma segura
*/
private static function handle_post_request() {
// Verificar permisos
if (!current_user_can('manage_options')) {
self::store_error('Permisos insuficientes');
self::redirect_back();
return;
}
// Verificar nonce
if (empty($_POST['_wpnonce']) || !wp_verify_nonce($_POST['_wpnonce'], 'simply_code_actions')) {
self::store_error('Error de seguridad');
self::redirect_back();
return;
}
$edit_mode = isset($_GET['edit']);
$form_data = self::sanitize_form_data($_POST);
// Validar datos
$validation_result = self::validate_form_data($form_data, $edit_mode);
if ($validation_result !== true) {
self::store_error($validation_result);
self::redirect_back();
return;
}
// Aplicar plantilla si es necesario
if (!$edit_mode && !empty($form_data['template'])) {
$form_data = self::apply_template($form_data);
}
// Determinar estado activo
$active = $edit_mode ?
self::get_existing_active_status($form_data['snippet_name']) :
true;
// Guardar snippet
$save_result = Simply_Snippet_Manager::save_snippet(
$form_data['snippet_name'],
$form_data['php_code'],
$form_data['js_code'],
$form_data['css_code'],
$form_data['description'],
$active,
$form_data['hook_priorities']
);
if (!$save_result) {
self::store_error('Error al guardar el snippet. Verifica los permisos de escritura y revisa debug.log.');
self::redirect_back();
return;
}
self::redirect_with_success($edit_mode, $form_data['snippet_name']);
}
// Agregar AJAX handler para detección de hooks
public static function ajax_detect_hooks() {
check_ajax_referer('simply_code_detect_hooks', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('No tienes permisos suficientes');
}
$php_code = stripslashes($_POST['php_code'] ?? '');
$hooks = [];
$critical_hooks = [];
if (class_exists('Simply_Hook_Detector') && method_exists('Simply_Hook_Detector', 'detect_hooks')) {
$hooks = Simply_Hook_Detector::detect_hooks($php_code);
$critical_hooks = Simply_Hook_Detector::get_critical_hooks();
}
wp_send_json_success([
'hooks' => $hooks,
'critical_hooks' => $critical_hooks
]);
}
/**
* Sanitize form data
*/
private static function sanitize_form_data($post_data) {
$hook_priorities = [];
if (isset($post_data['hook_priorities']) && is_array($post_data['hook_priorities'])) {
foreach ($post_data['hook_priorities'] as $hook => $priority) {
$hook_priorities[sanitize_text_field($hook)] = (int)$priority;
}
}
return [
'snippet_name' => sanitize_file_name($post_data['snippet_name'] ?? ''),
'php_code' => stripslashes($post_data['php_code'] ?? ''),
'js_code' => stripslashes($post_data['js_code'] ?? ''),
'css_code' => stripslashes($post_data['css_code'] ?? ''),
'description' => stripslashes($post_data['description'] ?? ''),
'template' => sanitize_text_field($post_data['template'] ?? ''),
'hook_priorities' => $hook_priorities
];
}
/**
* Validate form data
*/
private static function validate_form_data($form_data, $edit_mode) {
// Check required fields
if (empty($form_data['snippet_name'])) {
return 'El nombre del snippet es requerido';
}
// Validar formato del nombre para prevenir XSS
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $form_data['snippet_name'])) {
return 'El nombre del snippet solo puede contener letras, números, guiones y guiones bajos';
}
// Check for duplicate names (creation mode only)
if (!$edit_mode && Simply_Snippet_Manager::get_snippet($form_data['snippet_name'])) {
return 'Ya existe un snippet con ese nombre';
}
// Validate PHP syntax (si existe la clase)
if (!empty($form_data['php_code']) && class_exists('Simply_Syntax_Checker') && method_exists('Simply_Syntax_Checker', 'validate_php')) {
$syntax_result = Simply_Syntax_Checker::validate_php($form_data['php_code']);
if (isset($syntax_result['valid']) && $syntax_result['valid'] === false) {
return 'Error de sintaxis en PHP: ' . ($syntax_result['message'] ?? 'error desconocido');
}
}
return true;
}
/**
* Apply template to form data
*/
private static function apply_template($form_data) {
$templates = self::get_templates();
if (isset($templates[$form_data['template']])) {
$template = $templates[$form_data['template']];
$form_data['php_code'] = $template['code'];
if (empty($form_data['description'])) {
$form_data['description'] = $template['description'];
}
}
return $form_data;
}
/**
* Get existing snippet active status
*/
private static function get_existing_active_status($snippet_name) {
$existing_snippet = Simply_Snippet_Manager::get_snippet($snippet_name);
return $existing_snippet ? $existing_snippet['active'] : true;
}
/**
* Almacena un error para mostrarlo después del redirect
*/
private static function store_error($message) {
set_transient('simply_code_error', $message, 45);
}
/**
* Muestra los errores almacenados
*/
private static function show_stored_errors() {
$error = get_transient('simply_code_error');
if ($error) {
self::show_error($error);
delete_transient('simply_code_error');
}
}
/**
* Redirecciona de vuelta a la página anterior
*/
private static function redirect_back() {
$redirect_url = wp_get_referer();
if (!$redirect_url) {
$redirect_url = admin_url('admin.php?page=simply-code');
}
self::safe_redirect($redirect_url);
}
/**
* Realiza un redirect seguro
*/
private static function safe_redirect($url) {
// Solo limpiar si hay buffers de usuario con contenido
if (ob_get_level() > 0 && ob_get_contents() !== false) {
$buffer_content = ob_get_contents();
if (!empty(trim($buffer_content))) {
@ob_end_clean();
}
}
// Verificar que no se hayan enviado headers
if (headers_sent($file, $line)) {
error_log("Simply Code: Headers already sent in {$file} at line {$line}, cannot redirect to: {$url}");
echo '';
exit;
}
// Usar wp_safe_redirect con verificación
$result = wp_safe_redirect($url);
if (!$result) {
error_log("Simply Code: wp_safe_redirect failed for URL: {$url}");
echo '';
}
exit;
}
/**
* Redirect with success message
*/
private static function redirect_with_success($edit_mode, $snippet_name) {
$action_text = $edit_mode ? 'actualizado' : 'creado';
$success_message = sprintf('Snippet "%s" %s correctamente.', esc_html($snippet_name), $action_text);
set_transient('simply_code_success', $success_message, 45);
$redirect_url = admin_url('admin.php?page=simply-code');
self::safe_redirect($redirect_url);
}
/**
* Show error message
*/
private static function show_error($message) {
echo '<div class="notice notice-error inline"><p>' . esc_html($message) . '</p></div>';
}
/**
* Render the view
*/
private static function render_view($view_data) {
extract($view_data);
include SC_PATH . 'admin/views/snippet-editor.php';
}
/**
* Prepara los datos para la vista
*/
private static function prepare_view_data($edit_mode, $snippet_name) {
$templates = self::get_templates();
// Valores por defecto
$php_code = $templates['empty']['code'] ?? "<?php\n\n";
$js_code = '';
$css_code = '';
$description = '';
$snippet = null;
// Si estamos editando, cargar datos existentes
if ($edit_mode && !empty($snippet_name)) {
$snippet = Simply_Snippet_Manager::get_snippet($snippet_name);
if ($snippet) {
$php_code = $snippet['php'];
$js_code = $snippet['js'];
$css_code = $snippet['css'];
$description = $snippet['description'];
}
}
$hooks_data = [];
if ($edit_mode && $snippet) {
// Obtener metadatos del snippet
$json_file = rtrim(SC_STORAGE, '/\\') . "/snippets/{$snippet_name}.json";
if (file_exists($json_file)) {
$metadata = json_decode(file_get_contents($json_file), true) ?: [];
$hooks_data = $metadata['hooks'] ?? [];
}
}
// Retornar array con todas las variables necesarias para la vista
return [
'templates' => $templates,
'php_code' => $php_code,
'js_code' => $js_code,
'css_code' => $css_code,
'description' => $description,
'snippet' => $snippet,
'edit_mode' => $edit_mode,
'hooks_data' => $hooks_data,
'critical_hooks' => (class_exists('Simply_Hook_Detector') && method_exists('Simply_Hook_Detector', 'get_critical_hooks')) ? Simply_Hook_Detector::get_critical_hooks() : []
];
}
/**
* Get templates with caching
*/
public static function get_templates() {
if (self::$templates !== null) {
return self::$templates;
}
self::$templates = [
'empty' => [
'code' => "<?php\n// @description Nuevo snippet\n\nif (!defined('ABSPATH')) exit;\n\n",
'description' => 'Snippet vacío'
],
'function' => [
'code' => "<?php\n// @description Nueva función\n\nif (!defined('ABSPATH')) exit;\n\nfunction my_custom_function() {\n // Tu código aquí\n}\n",
'description' => 'Función personalizada'
],
'action' => [
'code' => "<?php\n// @description Nueva acción WordPress\n\nif (!defined('ABSPATH')) exit;\n\nadd_action('init', function() {\n // Tu código aquí\n});\n",
'description' => 'Acción de WordPress'
],
'filter' => [
'code' => "<?php\n// @description Nuevo filtro WordPress\n\nif (!defined('ABSPATH')) exit;\n\nadd_filter('the_content', function(\$content) {\n // Tu código aquí\n return \$content;\n});\n",
'description' => 'Filtro de WordPress'
]
];
return self::$templates;
}
}

View File

@ -0,0 +1,107 @@
<?php
if (!defined('ABSPATH')) exit;
$title = $edit_mode ? 'Editar Snippet' : 'Nuevo Snippet';
?>
<div class="wrap">
<h1><?php echo esc_html($title); ?></h1>
<?php if (!empty($snippet) && $edit_mode): ?>
<p><strong><?php echo esc_html($snippet['name']); ?></strong></p>
<?php endif; ?>
<form id="simply-code-form" method="post" action="">
<?php wp_nonce_field('simply_code_actions'); ?>
<table class="form-table">
<tr>
<th scope="row"><label for="snippet_name">Nombre del Snippet</label></th>
<td>
<input name="snippet_name" id="snippet_name" type="text" value="<?php echo esc_attr($snippet['name'] ?? ''); ?>" class="regular-text" required>
<p class="description">Sólo letras, números, guiones y guiones bajos.</p>
</td>
</tr>
<tr>
<th scope="row"><label for="description">Descripción</label></th>
<td>
<input name="description" id="description" type="text" value="<?php echo esc_attr($description); ?>" class="regular-text">
</td>
</tr>
<?php if (!$edit_mode): ?>
<tr>
<th scope="row"><label for="template">Plantilla</label></th>
<td>
<select name="template" id="template">
<option value="">Seleccionar plantilla...</option>
<?php foreach ($templates as $key => $template): ?>
<option value="<?php echo esc_attr($key); ?>"><?php echo esc_html($template['description']); ?></option>
<?php endforeach; ?>
</select>
</td>
</tr>
<?php endif; ?>
<tr>
<th scope="row">PHP</th>
<td>
<textarea name="php_code" id="php_code" rows="12" cols="80" class="large-text code"><?php echo esc_textarea($php_code); ?></textarea>
</td>
</tr>
<tr>
<th scope="row">JavaScript</th>
<td>
<textarea name="js_code" id="js_code" rows="6" cols="80" class="large-text code"><?php echo esc_textarea($js_code); ?></textarea>
</td>
</tr>
<tr>
<th scope="row">CSS</th>
<td>
<textarea name="css_code" id="css_code" rows="6" cols="80" class="large-text code"><?php echo esc_textarea($css_code); ?></textarea>
</td>
</tr>
<tr>
<th scope="row">Hooks detectados</th>
<td>
<div id="sc-hooks-list">
<?php if (!empty($hooks_data)): ?>
<?php foreach ($hooks_data as $hook_name => $hook_info): ?>
<div class="sc-hook-row">
<strong><?php echo esc_html($hook_name); ?></strong>
<span class="description"><?php echo esc_html($hook_info['type'] ?? ''); ?></span>
<br>
Prioridad: <input type="number" name="hook_priorities[<?php echo esc_attr($hook_name); ?>]" value="<?php echo esc_attr($hook_info['priority'] ?? 10); ?>" style="width:80px;">
<?php if (!empty($critical_hooks[$hook_name])): ?>
<span style="color:#d9534f; margin-left:8px;">⚠️ Hook crítico</span>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php else: ?>
<p class="description">Los hooks se detectan automáticamente. Puedes usar el botón para detectarlos.</p>
<?php endif; ?>
</div>
<p>
<button id="sc-detect-hooks" type="button" class="button">🔄 Detectar Hooks</button>
<span id="sc-detect-hooks-message" style="margin-left:10px;"></span>
</p>
<input type="hidden" id="sc-detect-nonce" value="<?php echo esc_attr(wp_create_nonce('simply_code_detect_hooks')); ?>">
<script>window.simply_code_ajax = window.simply_code_ajax || { ajax_url: "<?php echo esc_js(admin_url('admin-ajax.php')); ?>" };</script>
</td>
</tr>
</table>
<?php submit_button($edit_mode ? 'Actualizar Snippet' : 'Crear Snippet'); ?>
</form>
</div>
<!-- Cargar el script del editor si existe; si no, se usará ajaxurl ya definido por WP -->
<?php
// Encolar el script debería hacerse mediante admin_enqueue_scripts; este include es de respaldo
$editor_js = plugins_url('assets/js/editor.js', SC_PATH . 'simply-code.php');
echo '<script src="' . esc_url($editor_js) . '"></script>';
?>

View File

@ -0,0 +1,177 @@
<div class="wrap">
<h1>Simply Code</h1>
<style>
/* Toggle Switch Styles */
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
}
input:checked + .slider {
background-color: #2271b1;
}
input:focus + .slider {
box-shadow: 0 0 1px #2271b1;
}
input:checked + .slider:before {
transform: translateX(26px);
}
.slider.round {
border-radius: 24px;
}
.slider.round:before {
border-radius: 50%;
}
/* Inactive snippet row styling */
tr.inactive-snippet {
opacity: 0.6;
background-color: #f5f5f5;
}
/* Delete button styling */
.delete-button {
color: #dc3232;
}
.delete-button:hover {
color: #dc3232;
opacity: 0.8;
}
</style>
<?php
// Safe mode toggle
$safe_mode = get_option(Simply_Code_Admin::OPTION_SAFE_MODE, 'on');
?>
<div class="tablenav top">
<div class="alignleft actions">
<form method="post" style="display:inline;">
<?php wp_nonce_field('simply_code_actions'); ?>
<label>
<input type="checkbox" name="safe_mode" <?php echo $safe_mode === 'on' ? 'checked' : ''; ?> onChange="this.form.submit()">
Modo Seguro
</label>
<input type="hidden" name="safe_mode_toggle" value="1">
</form>
</div>
<div class="alignright">
<a href="<?php echo admin_url('admin.php?page=simply-code-new'); ?>" class="button button-primary">Nuevo Snippet</a>
</div>
<br class="clear">
</div>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>Nombre</th>
<th>Descripción</th>
<th>Estado</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<?php foreach ($snippets as $i => $snippet): ?>
<tr class="<?= isset($snippet['active']) && !$snippet['active'] ? 'inactive-snippet' : '' ?>">
<td><?= esc_html($snippet['name']) ?></td>
<td><?= esc_html($snippet['description']) ?></td>
<td>
<form method="post" style="display:inline;">
<?php wp_nonce_field('simply_code_actions'); ?>
<input type="hidden" name="snippet_name" value="<?= esc_attr($snippet['name']) ?>">
<label class="switch">
<input type="checkbox" name="snippet_active" <?= isset($snippet['active']) && $snippet['active'] ? 'checked' : '' ?> onChange="this.form.submit()">
<span class="slider round"></span>
</label>
<input type="hidden" name="toggle_snippet_status" value="1">
</form>
</td>
<td>
<?php if ($i > 0): ?>
<form method="post" style="display:inline;">
<?php wp_nonce_field('simply_code_actions'); ?>
<input type="hidden" name="move_up" value="<?= $i ?>">
<button type="submit" class="button" title="Subir"></button>
</form>
<?php endif; ?>
<?php if ($i < count($snippets) - 1): ?>
<form method="post" style="display:inline;">
<?php wp_nonce_field('simply_code_actions'); ?>
<input type="hidden" name="move_down" value="<?= $i ?>">
<button type="submit" class="button" title="Bajar"></button>
</form>
<?php endif; ?>
<a href="<?php echo add_query_arg(['page' => 'simply-code-new', 'edit' => $snippet['name']], admin_url('admin.php')); ?>" class="button" title="Editar"></a>
<form method="post" style="display:inline;" onsubmit="return confirm('¿Estás seguro de que deseas eliminar este snippet?');">
<?php wp_nonce_field('simply_code_actions'); ?>
<input type="hidden" name="snippet_name" value="<?= esc_attr($snippet['name']) ?>">
<input type="hidden" name="delete_snippet" value="1">
<button type="submit" class="button delete-button" title="Eliminar">🗑</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($snippets)): ?>
<tr>
<td colspan="4">No hay snippets disponibles.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<script>
// Confirmación adicional para eliminar snippets
document.addEventListener('DOMContentLoaded', function() {
const deleteForms = document.querySelectorAll('form[onsubmit*="confirm"]');
deleteForms.forEach(form => {
form.addEventListener('submit', function(e) {
const snippetName = this.querySelector('input[name="snippet_name"]').value;
if (!confirm(`¿Estás completamente seguro de que quieres eliminar el snippet "${snippetName}"?\n\nEsta acción eliminará permanentemente:\n- El código PHP\n- El código JavaScript\n- El código CSS\n- Todos los metadatos\n\nEsta acción NO se puede deshacer.`)) {
e.preventDefault();
return false;
}
});
});
});
</script>

View File

@ -0,0 +1,177 @@
<div class="wrap">
<h1>Simply Code</h1>
<style>
/* Toggle Switch Styles */
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
}
input:checked + .slider {
background-color: #2271b1;
}
input:focus + .slider {
box-shadow: 0 0 1px #2271b1;
}
input:checked + .slider:before {
transform: translateX(26px);
}
.slider.round {
border-radius: 24px;
}
.slider.round:before {
border-radius: 50%;
}
/* Inactive snippet row styling */
tr.inactive-snippet {
opacity: 0.6;
background-color: #f5f5f5;
}
/* Delete button styling */
.delete-button {
color: #dc3232;
}
.delete-button:hover {
color: #dc3232;
opacity: 0.8;
}
</style>
<?php
// Safe mode toggle
$safe_mode = get_option(Simply_Code_Admin::OPTION_SAFE_MODE, 'on');
?>
<div class="tablenav top">
<div class="alignleft actions">
<form method="post" style="display:inline;">
<?php wp_nonce_field('simply_code_actions'); ?>
<label>
<input type="checkbox" name="safe_mode" <?php echo $safe_mode === 'on' ? 'checked' : ''; ?> onChange="this.form.submit()">
Modo Seguro
</label>
<input type="hidden" name="safe_mode_toggle" value="1">
</form>
</div>
<div class="alignright">
<a href="<?php echo admin_url('admin.php?page=simply-code-new'); ?>" class="button button-primary">Nuevo Snippet</a>
</div>
<br class="clear">
</div>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>Nombre</th>
<th>Descripción</th>
<th>Estado</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<?php foreach ($snippets as $i => $snippet): ?>
<tr class="<?= isset($snippet['active']) && !$snippet['active'] ? 'inactive-snippet' : '' ?>">
<td><?= esc_html($snippet['name']) ?></td>
<td><?= esc_html($snippet['description']) ?></td>
<td>
<form method="post" style="display:inline;">
<?php wp_nonce_field('simply_code_actions'); ?>
<input type="hidden" name="snippet_name" value="<?= esc_attr($snippet['name']) ?>">
<label class="switch">
<input type="checkbox" name="snippet_active" <?= isset($snippet['active']) && $snippet['active'] ? 'checked' : '' ?> onChange="this.form.submit()">
<span class="slider round"></span>
</label>
<input type="hidden" name="toggle_snippet_status" value="1">
</form>
</td>
<td>
<?php if ($i > 0): ?>
<form method="post" style="display:inline;">
<?php wp_nonce_field('simply_code_actions'); ?>
<input type="hidden" name="move_up" value="<?= $i ?>">
<button type="submit" class="button" title="Subir"></button>
</form>
<?php endif; ?>
<?php if ($i < count($snippets) - 1): ?>
<form method="post" style="display:inline;">
<?php wp_nonce_field('simply_code_actions'); ?>
<input type="hidden" name="move_down" value="<?= $i ?>">
<button type="submit" class="button" title="Bajar"></button>
</form>
<?php endif; ?>
<a href="<?php echo add_query_arg(['page' => 'simply-code-new', 'edit' => $snippet['name']], admin_url('admin.php')); ?>" class="button" title="Editar"></a>
<form method="post" style="display:inline;" onsubmit="return confirm('¿Estás seguro de que deseas eliminar este snippet?');">
<?php wp_nonce_field('simply_code_actions'); ?>
<input type="hidden" name="snippet_name" value="<?= esc_attr($snippet['name']) ?>">
<input type="hidden" name="delete_snippet" value="1">
<button type="submit" class="button delete-button" title="Eliminar">🗑</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($snippets)): ?>
<tr>
<td colspan="4">No hay snippets disponibles.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<script>
// Confirmación adicional para eliminar snippets
document.addEventListener('DOMContentLoaded', function() {
const deleteForms = document.querySelectorAll('form[onsubmit*="confirm"]');
deleteForms.forEach(form => {
form.addEventListener('submit', function(e) {
const snippetName = this.querySelector('input[name="snippet_name"]').value;
if (!confirm(`¿Estás completamente seguro de que quieres eliminar el snippet "${snippetName}"?\n\nEsta acción eliminará permanentemente:\n- El código PHP\n- El código JavaScript\n- El código CSS\n- Todos los metadatos\n\nEsta acción NO se puede deshacer.`)) {
e.preventDefault();
return false;
}
});
});
});
</script>

39
assets/css/editor.css Normal file
View File

@ -0,0 +1,39 @@
.sc-editor-tabs ul {
list-style: none;
padding: 0;
margin: 20px 0 0;
border-bottom: 1px solid #ccc;
}
.sc-editor-tabs li {
display: inline-block;
padding: 10px 20px;
margin: 0;
cursor: pointer;
border: 1px solid transparent;
border-bottom: none;
margin-bottom: -1px;
}
.sc-editor-tabs li.active {
background: #fff;
border-color: #ccc;
border-bottom-color: #fff;
}
.tab-content {
display: none;
padding: 20px;
border: 1px solid #ccc;
border-top: none;
}
.tab-content.active {
display: block;
}
.tab-content textarea {
width: 100%;
margin: 0;
resize: vertical;
}

BIN
assets/images/banner.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

85
assets/js/editor.js Normal file
View File

@ -0,0 +1,85 @@
document.addEventListener('DOMContentLoaded', function() {
// pestañas (si las hay)
const tabs = document.querySelectorAll('.sc-editor-tabs li');
const contents = document.querySelectorAll('.tab-content');
if (tabs.length && contents.length) {
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(t => t.classList.remove('active'));
contents.forEach(c => c.classList.remove('active'));
tab.classList.add('active');
const id = 'tab-' + tab.dataset.tab;
const el = document.getElementById(id);
if (el) el.classList.add('active');
});
});
}
// Detección de hooks vía AJAX
const detectBtn = document.getElementById('sc-detect-hooks');
if (detectBtn) {
detectBtn.addEventListener('click', function() {
const phpCode = document.getElementById('php_code') ? document.getElementById('php_code').value : '';
const nonce = document.getElementById('sc-detect-nonce') ? document.getElementById('sc-detect-nonce').value : '';
const msgEl = document.getElementById('sc-detect-hooks-message');
if (msgEl) msgEl.textContent = 'Detectando...';
const ajaxUrl = (window.simply_code_ajax && window.simply_code_ajax.ajax_url) ? window.simply_code_ajax.ajax_url : (typeof ajaxurl !== 'undefined' ? ajaxurl : '/');
const form = new FormData();
form.append('action', 'simply_code_detect_hooks');
form.append('nonce', nonce);
form.append('php_code', phpCode);
fetch(ajaxUrl, {
method: 'POST',
credentials: 'same-origin',
body: form
}).then(function(resp) {
return resp.json();
}).then(function(data) {
if (!data) {
if (msgEl) msgEl.textContent = 'Respuesta inválida';
return;
}
if (data.success) {
const hooks = data.data.hooks || {};
const critical = data.data.critical_hooks || {};
const container = document.getElementById('sc-hooks-list');
if (container) {
container.innerHTML = '';
if (Object.keys(hooks).length === 0) {
container.innerHTML = '<p class="description">No se detectaron hooks.</p>';
} else {
for (const h in hooks) {
const info = hooks[h] || {};
const row = document.createElement('div');
row.className = 'sc-hook-row';
const html = '<strong>' + escapeHtml(h) + '</strong> <span class="description">' + escapeHtml(info.type || '') + '</span><br>' +
'Prioridad: <input type="number" name="hook_priorities[' + escapeAttr(h) + ']" value="' + (info.priority || 10) + '" style="width:80px;">' +
(critical[h] ? '<span style="color:#d9534f; margin-left:8px;">⚠️ Hook crítico</span>' : '');
row.innerHTML = html;
container.appendChild(row);
}
}
}
if (msgEl) msgEl.textContent = 'Hooks detectados';
} else {
if (msgEl) msgEl.textContent = data.data && data.data.message ? data.data.message : 'Error al detectar hooks';
}
}).catch(function(err) {
if (msgEl) msgEl.textContent = 'Error AJAX';
console.error(err);
});
});
}
// helpers
function escapeHtml(str) {
if (!str) return '';
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function escapeAttr(str) {
return escapeHtml(str).replace(/'/g, '&#39;');
}
});

View File

@ -0,0 +1,67 @@
<?php
class Simply_Hook_Detector {
/**
* Detectar hooks y sus prioridades existentes en código PHP
*/
public static function detect_hooks($php_code) {
$hooks = [];
// Regex para add_action/add_filter con prioridades opcionales
$pattern = '/add_(action|filter)\s*\(\s*[\'"]([^\'"]+)[\'"][\s,]*([^,)]+)?[\s,]*(\d+)?[\s,]*(\d+)?\s*\)/i';
if (preg_match_all($pattern, $php_code, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
$type = strtolower($match[1]);
$hook_name = $match[2];
$priority = isset($match[4]) && is_numeric($match[4]) ? (int)$match[4] : 10;
$accepted_args = isset($match[5]) && is_numeric($match[5]) ? (int)$match[5] : 1;
$hooks[] = [
'name' => $hook_name,
'type' => $type,
'priority' => $priority,
'accepted_args' => $accepted_args,
'auto_detected' => true
];
}
}
return $hooks;
}
/**
* Obtener hooks críticos que necesitan prioridades específicas
*/
public static function get_critical_hooks() {
return [
'init' => ['default_priority' => 10, 'description' => 'Inicialización temprana'],
'wp_loaded' => ['default_priority' => 10, 'description' => 'WordPress completamente cargado'],
'template_redirect' => ['default_priority' => 10, 'description' => 'Antes de cargar template'],
'wp_head' => ['default_priority' => 10, 'description' => 'En el head del HTML'],
'wp_footer' => ['default_priority' => 10, 'description' => 'En el footer del HTML'],
'wp_enqueue_scripts' => ['default_priority' => 10, 'description' => 'Encolar scripts frontend'],
'admin_enqueue_scripts' => ['default_priority' => 10, 'description' => 'Encolar scripts admin'],
'the_content' => ['default_priority' => 10, 'description' => 'Filtrar contenido'],
'wp_title' => ['default_priority' => 10, 'description' => 'Filtrar título']
];
}
/**
* Calcular prioridad de carga del snippet
*/
public static function calculate_load_priority($hooks) {
if (empty($hooks)) return 10;
$critical_hooks = ['init', 'after_setup_theme', 'wp_loaded'];
$min_priority = 999;
foreach ($hooks as $hook_data) {
if (in_array($hook_data['name'], $critical_hooks)) {
$min_priority = min($min_priority, $hook_data['priority']);
}
}
return $min_priority === 999 ? 10 : $min_priority;
}
}

View File

@ -0,0 +1,495 @@
<?php
/**
* Simply Code - Snippet Manager
*/
class Simply_Snippet_Manager {
private static $order_cache = null;
private static $snippets_cache = null;
private static $cache_version = '1.0';
/**
* Get snippets order with improved caching
*/
private static function get_order() {
if (self::$order_cache !== null) {
return self::$order_cache;
}
$order_file = SC_PATH . 'includes/snippets-order.php';
if (!file_exists($order_file)) {
self::$order_cache = [];
return [];
}
// Use WordPress transients for better caching
$cache_key = 'simply_code_order_' . md5_file($order_file);
$order = get_transient($cache_key);
if ($order === false) {
$content = file_get_contents($order_file);
$order = [];
if (preg_match('/return\s+(.+);/s', $content, $matches)) {
// Eval only the return expression (careful in general, but original logic used this)
$order = @eval('return ' . $matches[1] . ';');
if (!is_array($order)) {
$order = [];
}
}
set_transient($cache_key, $order, HOUR_IN_SECONDS);
}
self::$order_cache = $order;
return $order;
}
/**
* Improved cache invalidation
*/
private static function invalidate_cache() {
self::$order_cache = null;
self::$snippets_cache = null;
// Clear WordPress transients with proper SQL query
global $wpdb;
// Note: uses direct deletion of transient rows; WP provides delete_transient but we want to clear plugin-specific keys
$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_simply_code_%'" );
$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_simply_code_%'" );
// Clear filesystem cache
clearstatcache();
// Clear opcache if available
if (function_exists('opcache_reset')) {
@opcache_reset();
}
}
/**
* Save snippet with hooks priority support
*/
public static function save_snippet($name, $php, $js, $css, $description = '', $active = true, $hook_priorities = []) {
try {
// Crear directorios si no existen
$directories = [
rtrim(SC_STORAGE, '/\\') . '/snippets/',
rtrim(SC_STORAGE, '/\\') . '/js/',
rtrim(SC_STORAGE, '/\\') . '/css/',
rtrim(SC_STORAGE, '/\\') . '/backups/'
];
foreach ($directories as $dir) {
if (!is_dir($dir)) {
if (!mkdir($dir, 0755, true) && !is_dir($dir)) {
error_log("Simply Code: Failed to create directory: {$dir}");
return false;
}
}
}
// Detectar hooks automáticamente (si existe la clase detector)
$detected_hooks = [];
if (class_exists('Simply_Hook_Detector') && method_exists('Simply_Hook_Detector', 'detect_hooks')) {
$detected_hooks = Simply_Hook_Detector::detect_hooks($php);
}
// Combinar con prioridades configuradas manualmente
$final_hooks = [];
foreach ($detected_hooks as $hook) {
$hook_name = $hook['name'];
$final_hooks[$hook_name] = [
'type' => $hook['type'],
'priority' => isset($hook_priorities[$hook_name]) ? (int)$hook_priorities[$hook_name] : ($hook['priority'] ?? 10),
'accepted_args' => $hook['accepted_args'] ?? 1,
'auto_detected' => true
];
}
// Agregar hooks configurados manualmente que no fueron detectados
foreach ($hook_priorities as $hook_name => $priority) {
if (!isset($final_hooks[$hook_name])) {
$final_hooks[$hook_name] = [
'type' => 'manual',
'priority' => (int)$priority,
'accepted_args' => 1,
'auto_detected' => false
];
}
}
// Crear metadatos completos
$metadata = [
'description' => $description,
'last_updated' => date('Y-m-d H:i:s'),
'active' => (bool) $active,
'hooks' => $final_hooks,
'load_priority' => (class_exists('Simply_Hook_Detector') && method_exists('Simply_Hook_Detector', 'calculate_load_priority'))
? Simply_Hook_Detector::calculate_load_priority($final_hooks)
: 10
];
// Crear backup si existe el archivo original
$php_file = rtrim(SC_STORAGE, '/\\') . "/snippets/{$name}.php";
$backup_dir = rtrim(SC_STORAGE, '/\\') . "/backups/";
if (file_exists($php_file)) {
$backup_file = $backup_dir . $name . '.php.' . time();
if (!@copy($php_file, $backup_file)) {
error_log("Simply Code: Failed to create backup for {$name}");
}
}
// Guardar todos los archivos con verificación de errores
$files_to_save = [
rtrim(SC_STORAGE, '/\\') . "/snippets/{$name}.php" => $php,
rtrim(SC_STORAGE, '/\\') . "/js/{$name}.js" => $js,
rtrim(SC_STORAGE, '/\\') . "/css/{$name}.css" => $css,
rtrim(SC_STORAGE, '/\\') . "/snippets/{$name}.json" => json_encode($metadata, JSON_PRETTY_PRINT)
];
$all_success = true;
foreach ($files_to_save as $file_path => $content) {
$dir_of_file = dirname($file_path);
if (!is_dir($dir_of_file)) {
if (!mkdir($dir_of_file, 0755, true) && !is_dir($dir_of_file)) {
error_log("Simply Code: Failed to create directory for file: {$dir_of_file}");
$all_success = false;
continue;
}
}
$result = @file_put_contents($file_path, $content, LOCK_EX);
if ($result === false) {
error_log("Simply Code: Failed to write file: {$file_path}");
$all_success = false;
}
}
if ($all_success) {
self::invalidate_cache();
return true;
}
return false;
} catch (Throwable $e) {
error_log("Simply Code: Error saving snippet {$name} - " . $e->getMessage());
return false;
}
}
/**
* Load snippets with priority ordering
*/
public static function load_snippets() {
$dir = rtrim(SC_STORAGE, '/\\') . '/snippets/';
if (!is_dir($dir)) return;
$all_snippets = self::get_snippets_with_priorities();
$safe_mode = (get_option(defined('Simply_Code_Admin::OPTION_SAFE_MODE') ? Simply_Code_Admin::OPTION_SAFE_MODE : 'simply_code_safe_mode', 'on') === 'on');
foreach ($all_snippets as $snippet_data) {
if (!self::load_single_snippet($snippet_data['name'], $safe_mode)) {
error_log("Simply Code: Failed to load snippet: {$snippet_data['name']}");
}
}
}
/**
* Get snippets ordered by priority
*/
private static function get_snippets_with_priorities() {
$dir = rtrim(SC_STORAGE, '/\\') . '/snippets/';
$snippets = [];
$order = self::get_order();
// Recopilar todos los snippets con sus metadatos
foreach (glob($dir . '*.php') as $file) {
$name = basename($file, '.php');
$json_file = $dir . $name . '.json';
$metadata = [];
if (file_exists($json_file)) {
$metadata = json_decode(file_get_contents($json_file), true) ?: [];
}
if (($metadata['active'] ?? true)) {
$order_index = array_search($name, $order);
$snippets[] = [
'name' => $name,
'load_priority' => $metadata['load_priority'] ?? 10,
'order_index' => $order_index !== false ? $order_index : 999
];
}
}
// Ordenar primero por load_priority, luego por order_index
usort($snippets, function($a, $b) {
if ($a['load_priority'] === $b['load_priority']) {
return $a['order_index'] <=> $b['order_index'];
}
return $a['load_priority'] <=> $b['load_priority'];
});
return $snippets;
}
/**
* Load a single snippet with proper error handling
*/
private static function load_single_snippet($basename, $safe_mode = true) {
$php_file = rtrim(SC_STORAGE, '/\\') . "/snippets/{$basename}.php";
$json_file = rtrim(SC_STORAGE, '/\\') . "/snippets/{$basename}.json";
// Check if snippet is active
if (!self::is_snippet_active($json_file)) {
return false;
}
if (!file_exists($php_file)) {
return false;
}
try {
if ($safe_mode && class_exists('Simply_Syntax_Checker') && method_exists('Simply_Syntax_Checker', 'validate_php')) {
$snippet_code = file_get_contents($php_file);
$syntax_result = Simply_Syntax_Checker::validate_php($snippet_code);
if (isset($syntax_result['valid']) && !$syntax_result['valid']) {
error_log("Simply Code: Syntax error in {$php_file} - " . ($syntax_result['message'] ?? 'unknown'));
return false;
}
}
require_once $php_file;
return true;
} catch (Throwable $e) {
error_log("Simply Code: Error loading {$php_file} - " . $e->getMessage());
return false;
}
}
/**
* Check if snippet is active
*/
private static function is_snippet_active($json_file) {
if (!file_exists($json_file)) {
return true; // Default to active if no metadata
}
$meta = json_decode(file_get_contents($json_file), true);
return !isset($meta['active']) || $meta['active'] === true;
}
/**
* Enqueue snippet JS and CSS assets for active snippets
*/
public static function enqueue_snippet_assets() {
$dir_js = rtrim(SC_STORAGE, '/\\') . '/js/';
$dir_css = rtrim(SC_STORAGE, '/\\') . '/css/';
$snippets = self::list_snippets(true);
foreach ($snippets as $snippet) {
if (empty($snippet['active'])) continue;
$name = $snippet['name'];
// JS
$js_file = $dir_js . $name . '.js';
if (file_exists($js_file)) {
wp_enqueue_script(
'simply-snippet-' . $name,
plugins_url('storage/js/' . $name . '.js', SC_PATH . 'simply-code.php'),
['jquery'],
filemtime($js_file),
true
);
}
// CSS
$css_file = $dir_css . $name . '.css';
if (file_exists($css_file)) {
wp_enqueue_style(
'simply-snippet-' . $name,
plugins_url('storage/css/' . $name . '.css', SC_PATH . 'simply-code.php'),
[],
filemtime($css_file)
);
}
}
}
/**
* Toggle snippet status
*/
public static function toggle_snippet_status($name, $active) {
$json_file = rtrim(SC_STORAGE, '/\\') . "/snippets/{$name}.json";
$snippet_data = [];
// Clear filesystem cache
clearstatcache(true, $json_file);
// Get existing data
if (file_exists($json_file)) {
$content = file_get_contents($json_file);
$snippet_data = json_decode($content, true) ?: [];
}
// Update status
$snippet_data['active'] = (bool) $active;
$snippet_data['last_updated'] = date('Y-m-d H:i:s');
// Force immediate write with JSON_PRETTY_PRINT
$success = @file_put_contents($json_file, json_encode($snippet_data, JSON_PRETTY_PRINT), LOCK_EX);
if ($success !== false) {
// Clear cache after writing
clearstatcache(true, $json_file);
// Verify the content was written correctly
$verify_content = file_get_contents($json_file);
$verify_data = json_decode($verify_content, true);
if ($verify_data === null || !isset($verify_data['active']) || $verify_data['active'] !== (bool) $active) {
error_log("Simply Code: Status toggle verification failed for {$name}");
return false;
}
self::invalidate_cache();
return true;
}
return false;
}
/**
* List all snippets with improved caching
*/
public static function list_snippets($apply_order = true, $force_reload = false) {
// Si no se fuerza la recarga y hay caché, devolverlo
if (self::$snippets_cache !== null && !$force_reload) {
return self::$snippets_cache;
}
$snippets = [];
$dir = rtrim(SC_STORAGE, '/\\') . '/snippets/';
if (!is_dir($dir)) {
self::$snippets_cache = $snippets;
return $snippets;
}
// Obtener snippets y orden
$order = $apply_order ? self::get_order() : [];
$available_snippets = [];
foreach (scandir($dir) as $file) {
if (pathinfo($file, PATHINFO_EXTENSION) === 'php') {
$name = basename($file, '.php');
$json_file = $dir . $name . '.json';
$meta = [];
if (file_exists($json_file)) {
$content = file_get_contents($json_file);
$meta = json_decode($content, true) ?: [];
}
$available_snippets[$name] = [
'name' => $name,
'description' => $meta['description'] ?? '',
'active' => $meta['active'] ?? true
];
}
}
// Aplicar orden si se solicita
if ($apply_order && !empty($order)) {
$ordered_snippets = [];
foreach ($order as $name) {
if (isset($available_snippets[$name])) {
$ordered_snippets[] = $available_snippets[$name];
unset($available_snippets[$name]);
}
}
// Agregar snippets no ordenados al final
$snippets = array_merge($ordered_snippets, array_values($available_snippets));
} else {
$snippets = array_values($available_snippets);
}
// Guardar en caché
self::$snippets_cache = $snippets;
return $snippets;
}
/**
* Update snippets order with safer file write
*/
public static function update_snippets_order($names) {
$order_file = SC_PATH . 'includes/snippets-order.php';
$order_dir = dirname($order_file);
if (!is_dir($order_dir)) {
if (!mkdir($order_dir, 0755, true) && !is_dir($order_dir)) {
error_log("Simply Code: Failed to create directory for order file: {$order_dir}");
return false;
}
}
$content = "<?php\nreturn " . var_export($names, true) . ";\n";
$result = @file_put_contents($order_file, $content, LOCK_EX);
if ($result !== false) {
// Invalidate opcache if present
if (function_exists('opcache_invalidate')) {
@opcache_invalidate($order_file, true);
}
self::invalidate_cache();
return true;
}
return false;
}
/**
* Get a specific snippet
*/
public static function get_snippet($name) {
$php_file = rtrim(SC_STORAGE, '/\\') . "/snippets/{$name}.php";
$js_file = rtrim(SC_STORAGE, '/\\') . "/js/{$name}.js";
$css_file = rtrim(SC_STORAGE, '/\\') . "/css/{$name}.css";
$json_file = rtrim(SC_STORAGE, '/\\') . "/snippets/{$name}.json";
// Verificar si existe el archivo PHP principal
if (!file_exists($php_file)) {
return null;
}
// Obtener metadatos
$desc = '';
$active = true;
if (file_exists($json_file)) {
$content = file_get_contents($json_file);
$meta = json_decode($content, true) ?: [];
$desc = $meta['description'] ?? '';
$active = isset($meta['active']) ? $meta['active'] : true;
}
// Leer contenido de los archivos
return [
'name' => $name,
'php' => file_exists($php_file) ? file_get_contents($php_file) : '',
'js' => file_exists($js_file) ? file_get_contents($js_file) : '',
'css' => file_exists($css_file) ? file_get_contents($css_file) : '',
'description' => $desc,
'active' => $active
];
}
/**
* Delete snippet with improved cleanup
*/
public static function delete_snippet($name) {
$files_to_delete = [
rtrim(SC_STORAGE, '/\\') . "/snippets/{$name}.php",
rtrim(SC_STORAGE, '/\\') . "/snippets/{$name}.json",
rtrim(SC_STORAGE, '/\\') . "/js/{$name}.js",
rtrim(SC_STORAGE, '/\\') . "/css/{$name}.css"
];
$success = true;
foreach ($files_to_delete as $file) {
if (file_exists($file) && !@unlink($file)) {
$success = false;
error_log("Simply Code: Failed to delete file: {$file}");
}
}
if ($success) {
self::invalidate_cache();
}
return $success;
}
}

View File

@ -0,0 +1,66 @@
<?php
/**
* Class to check PHP syntax before saving hook files
*/
class Simply_Syntax_Checker {
/**
* Check if PHP code has valid syntax
*
* @param string $code The PHP code to check
* @return array ['valid' => bool, 'message' => string]
*/
public static function check_php($code) {
// Create a temporary file
$temp_file = tempnam(sys_get_temp_dir(), 'sh_syntax_');
file_put_contents($temp_file, $code);
// Check syntax using PHP's built-in linter
$output = [];
$return_var = 0;
exec("php -l " . escapeshellarg($temp_file) . " 2>&1", $output, $return_var);
// Clean up
unlink($temp_file);
// Process result
if ($return_var !== 0) {
$error_msg = implode("\n", $output);
// Clean up the error message to remove the temp filename
$error_msg = preg_replace('/in ' . preg_quote($temp_file, '/') . ' on/', 'on', $error_msg);
return [
'valid' => false,
'message' => $error_msg
];
}
return [
'valid' => true,
'message' => 'Syntax is valid'
];
}
/**
* Validate a hook before saving
*
* @param string $php_code The PHP code to validate
* @return array ['valid' => bool, 'message' => string]
*/
public static function validate_php($php_code) {
// Check if PHP code is empty or only whitespace
if (empty(trim($php_code))) {
return [
'valid' => true,
'message' => 'Empty PHP code is valid'
];
}
// If code doesn't start with <?php, add it for validation purposes
if (strpos(trim($php_code), '<?php') !== 0) {
$php_code = "<?php\n" . $php_code;
}
return self::check_php($php_code);
}
}

View File

@ -0,0 +1,17 @@
<?php
return array (
0 => 'theme-scripts',
1 => 'back-to-top-button',
2 => 'pmpro-add-fields-to-signup',
3 => 'pmpro-dynamic-pricing',
4 => 'bcc-admin-emails',
5 => 'pmpro-pricing-tables',
6 => 'current-year',
7 => 'simple-accordion',
8 => 'smtp-settings',
9 => 'custom-login-logo',
10 => 'auto-redirect-after-logout',
11 => 'custom-pmpro-email-sender-name',
12 => 'pmpro-brl-currency-format',
13 => 'pmpro-hide-toolbar',
);

33
readme.txt Normal file
View File

@ -0,0 +1,33 @@
=== Simply Code ===
Contributors: DavidCamejo
Tags: snippets, code, modular, functions.php, admin, developer
Requires at least: 5.6
Tested up to: 6.5
Requires PHP: 7.4
Stable tag: 3.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Gestión modular de código personalizado como mini-plugins. La alternativa moderna a functions.php.
== Descripción ==
Simply Code es un plugin minimalista para WordPress que permite gestionar snippets de código como módulos independientes, sin depender de la base de datos. Organiza, activa/desactiva y edita tus fragmentos de PHP, JS y CSS desde una interfaz profesional y segura.
== Instalación ==
1. Sube la carpeta `simply-code` al directorio `/wp-content/plugins/`.
2. Activa el plugin desde el menú 'Plugins' de WordPress.
3. Accede al menú "Simply Code" para comenzar a gestionar tus snippets.
== Características principales ==
- Alternativa moderna a functions.php
- Activación/desactivación instantánea de snippets
- Sin dependencia de base de datos
- Validación de sintaxis antes de guardar
- Soporte para PHP, JavaScript y CSS
- Compatible con WordPress Multisite
== Changelog ==
Ver archivo CHANGELOG.md para detalles de cambios.
== Créditos ==
Desarrollado por David Camejo & AI.

39
simply-code.php Normal file
View File

@ -0,0 +1,39 @@
<?php
/*
Plugin Name: Simply Code
Description: Gestión modular de código personalizado como mini-plugins. La alternativa moderna a functions.php.
Version: 3.5.0
Author: David Camejo & AI
*/
if (!defined('ABSPATH')) exit;
define('SC_PATH', plugin_dir_path(__FILE__));
define('SC_URL', plugin_dir_url(__FILE__));
define('SC_STORAGE', SC_PATH . 'storage');
// Crear carpetas necesarias si no existen
$folders = [
SC_STORAGE . '/snippets/',
SC_STORAGE . '/js/',
SC_STORAGE . '/css/',
SC_PATH . 'templates/',
];
foreach ($folders as $folder) {
if (!is_dir($folder)) {
mkdir($folder, 0755, true);
}
}
// Cargar clases principales
require_once SC_PATH . 'includes/class-snippet-manager.php';
require_once SC_PATH . 'includes/class-syntax-checker.php';
require_once SC_PATH . 'admin/class-admin-page.php';
require_once SC_PATH . 'admin/class-snippet-editor.php';
require_once SC_PATH . 'includes/class-hook-detector.php';
// Registrar acciones principales
add_action('after_setup_theme', ['Simply_Snippet_Manager', 'load_snippets'], 5);
add_action('admin_menu', ['Simply_Code_Admin', 'register_menu']);
add_action('wp_enqueue_scripts', ['Simply_Snippet_Manager', 'enqueue_snippet_assets']);
add_action('wp_ajax_simply_code_detect_hooks', ['Simply_Snippet_Editor', 'ajax_detect_hooks']);

View File

@ -0,0 +1,23 @@
/* Back to Top Button CSS */
#toTop {
display: none;
background-color: #15509E;
color: #fff;
font-weight: 600;
font-size: 150%;
opacity: 0.4;
position: fixed;
align-items: center;
padding-top: .3em;
border-radius: 3px;
cursor:pointer;
bottom: .35em;
right: .5em;
width: 1.5em;
height: 1.2em;
z-index: 999;
}
#toTop:hover {
opacity: 0.7;
}

View File

View File

View File

View File

@ -0,0 +1,909 @@
/* Dynamic pricing for PMPro CSS - VERSIÓN COMPLETA OPTIMIZADA v2.1 */
/* ====================================================================
RESET Y BASE
==================================================================== */
/* Prevenir desbordamiento horizontal global */
* {
box-sizing: border-box !important;
}
body {
overflow-x: hidden !important;
}
/* ====================================================================
ESTILOS GENERALES DEL FORMULARIO
==================================================================== */
#total_price_display {
background-color: #edf5f9 !important;
border: none !important;
font-size: 20px !important;
font-weight: 600 !important;
cursor: default !important;
pointer-events: none !important;
color: #2d5a87 !important;
text-align: center !important;
max-width: 100% !important;
width: 100% !important;
padding: 12px !important;
border-radius: 6px !important;
box-sizing: border-box !important;
}
.pmpro_account-profile-field {
margin: 20px 0 !important;
padding: 15px !important;
background: #f8f8f8 !important;
border-radius: 5px !important;
border-left: 4px solid #2d9cdb !important;
max-width: 100% !important;
box-sizing: border-box !important;
overflow-x: hidden !important;
}
.pmpro_nextcloud_config_details p {
margin: 8px 0 !important;
line-height: 1.4 !important;
word-wrap: break-word !important;
overflow-wrap: break-word !important;
}
.pmpro_nextcloud_config_details h3 {
color: #2d5a87 !important;
margin-bottom: 15px !important;
border-bottom: 2px solid #e0e0e0 !important;
padding-bottom: 8px !important;
word-wrap: break-word !important;
overflow-wrap: break-word !important;
}
/* ====================================================================
CONTENEDORES PRINCIPALES - CORRECCIÓN PARA MÓVILES
==================================================================== */
/* Contenedor principal del formulario */
#pmpro_checkout,
.pmpro_checkout,
.pmpro_billing,
.pmpro_account {
max-width: 100% !important;
overflow-x: hidden !important;
box-sizing: border-box !important;
}
/* Fieldsets responsive */
fieldset {
max-width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
overflow-x: hidden !important;
}
fieldset legend {
max-width: calc(100% - 20px) !important;
padding: 0 10px !important;
font-size: 16px !important;
font-weight: 600 !important;
word-wrap: break-word !important;
overflow-wrap: break-word !important;
box-sizing: border-box !important;
}
/* Campos individuales */
.pmpro_checkout-field,
.pmpro_billing-field,
.pmpro_account-field {
max-width: 100% !important;
box-sizing: border-box !important;
overflow-x: hidden !important;
margin: 15px 0 !important;
padding: 0 !important;
}
/* ====================================================================
SELECTS - OPTIMIZADOS PARA MÓVILES
==================================================================== */
/* Base para todos los selects con flecha única */
#storage_space,
#num_users,
#payment_frequency,
select#num_users,
select#storage_space,
select#payment_frequency {
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
padding: 12px 40px 12px 15px !important;
border: 1px solid #ddd !important;
border-radius: 6px !important;
font-size: 14px !important;
line-height: 1.4 !important;
transition: border-color 0.3s ease !important;
box-sizing: border-box !important;
/* Remover estilos nativos */
-webkit-appearance: none !important;
-moz-appearance: none !important;
-ms-appearance: none !important;
appearance: none !important;
/* Flecha personalizada única */
background-color: white !important;
background-image: url("data:image/svg+xml;charset=US-ASCII,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'><path fill='%23666' d='m0 0 2 2 2-2z'/></svg>") !important;
background-repeat: no-repeat !important;
background-position: right 15px center !important;
background-size: 12px !important;
}
/* Selectores con máxima especificidad */
html body .pmpro_checkout-field select,
html body .pmpro_billing-field select,
html body .pmpro_account-field select,
html body #num_users,
html body #storage_space,
html body #payment_frequency,
body .pmpro_checkout-field select#num_users,
body .pmpro_billing-field select#num_users,
body .pmpro_account-field select#num_users {
-webkit-appearance: none !important;
-moz-appearance: none !important;
-ms-appearance: none !important;
appearance: none !important;
background-image: url("data:image/svg+xml;charset=US-ASCII,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'><path fill='%23666' d='m0 0 2 2 2-2z'/></svg>") !important;
background-repeat: no-repeat !important;
background-position: right 15px center !important;
background-size: 12px !important;
background-color: #cadbe6 !important;
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
border: 1px solid #777 !important;
}
/* Estados de focus */
#storage_space:focus,
#num_users:focus,
#payment_frequency:focus {
border-color: #2d9cdb !important;
outline: none !important;
box-shadow: 0 0 0 2px rgba(45, 156, 219, 0.1) !important;
}
/* ====================================================================
OPCIONES DEL SELECT
==================================================================== */
#num_users option,
#storage_space option,
#payment_frequency option {
padding: 10px 15px !important;
white-space: normal !important;
overflow: visible !important;
min-height: 40px !important;
line-height: 1.4 !important;
word-wrap: break-word !important;
overflow-wrap: break-word !important;
-webkit-text-size-adjust: none !important;
-moz-text-size-adjust: none !important;
-ms-text-size-adjust: none !important;
text-size-adjust: none !important;
}
/* Opciones deshabilitadas */
#storage_space option:disabled,
#num_users option:disabled,
#payment_frequency option:disabled {
color: #999 !important;
background-color: #f5f5f5 !important;
}
/* ====================================================================
CAMPO DE SUITE OFIMÁTICA CON BORDE
==================================================================== */
.pmpro_checkout-field-office-suite.bordered-field {
border: 1px solid #2d9cdb !important;
border-radius: 8px !important;
padding: 16px !important;
background-color: #f9fbff !important;
margin: 20px 0 !important;
position: relative !important;
overflow-x: hidden !important;
max-width: 100% !important;
box-sizing: border-box !important;
}
.pmpro_checkout-field-office-suite.bordered-field label {
font-weight: 600 !important;
color: #2d5a87 !important;
display: block !important;
position: relative !important;
word-wrap: break-word !important;
overflow-wrap: break-word !important;
max-width: 100% !important;
box-sizing: border-box !important;
}
/* ====================================================================
SISTEMA DE TOOLTIPS
==================================================================== */
.pmpro-tooltip-trigger {
color: #2d9cdb !important;
cursor: help !important;
font-size: 22px !important;
vertical-align: top !important;
display: inline-block !important;
width: 20px !important;
height: 20px !important;
transition: all 0.2s ease !important;
position: relative !important;
z-index: 1 !important;
}
.pmpro-tooltip-trigger:hover {
color: #1e6091 !important;
transform: scale(1.1) !important;
}
.pmpro-tooltip {
position: absolute !important;
background-color: #333 !important;
color: white !important;
padding: 16px 20px !important;
border-radius: 8px !important;
font-size: 13px !important;
line-height: 1.1 !important;
width: 400px !important;
max-width: 95vw !important;
z-index: 10001 !important;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2) !important;
word-wrap: break-word !important;
word-break: normal !important;
overflow-wrap: break-word !important;
white-space: normal !important;
text-align: left !important;
box-sizing: border-box !important;
pointer-events: auto !important;
transition: opacity 0.3s ease, visibility 0.3s ease !important;
}
.pmpro-tooltip p {
margin: 0 0 12px 0 !important;
padding: 0 !important;
}
.pmpro-tooltip p:last-child {
margin-bottom: 0 !important;
}
.pmpro-tooltip strong {
color: #fff !important;
font-weight: 600 !important;
display: inline !important;
}
/* ====================================================================
MENSAJES DE ALERTA
==================================================================== */
.pmpro_message {
padding: 12px 16px !important;
margin: 16px 0 !important;
border-radius: 6px !important;
font-size: 14px !important;
max-width: 100% !important;
box-sizing: border-box !important;
word-wrap: break-word !important;
overflow-wrap: break-word !important;
}
.pmpro_message.pmpro_error {
background-color: #fdf2f2 !important;
border: 1px solid #fecaca !important;
color: #dc2626 !important;
}
/* ====================================================================
BREAKPOINT TABLET (768px)
==================================================================== */
@media (max-width: 768px) {
/* Contenedor principal más compacto */
fieldset {
padding: 12px 8px !important;
}
fieldset legend {
font-size: 15px !important;
max-width: calc(100% - 16px) !important;
padding: 0 8px !important;
}
.pmpro_checkout-field,
.pmpro_billing-field,
.pmpro_account-field {
margin: 12px 0 !important;
padding: 0 5px !important;
}
/* Selects responsive */
#storage_space,
#num_users,
#payment_frequency,
select#storage_space,
select#num_users,
select#payment_frequency {
padding: 10px 35px 10px 12px !important;
font-size: 14px !important;
background-size: 11px !important;
background-position: right 12px center !important;
}
#num_users option,
#storage_space option,
#payment_frequency option {
font-size: 13px !important;
padding: 8px 12px !important;
min-height: 35px !important;
line-height: 1.3 !important;
}
/* Campo de precio responsive */
#total_price_display {
font-size: 18px !important;
padding: 10px !important;
}
/* Sección bordered-field responsive */
.pmpro_checkout-field-office-suite.bordered-field {
padding: 12px !important;
margin: 15px 0 !important;
border-radius: 6px !important;
}
.pmpro_checkout-field-office-suite.bordered-field label {
font-size: 16px !important;
margin-bottom: 8px !important;
}
/* Tooltips responsive */
.pmpro-tooltip {
width: 350px !important;
max-width: calc(100vw - 20px) !important;
font-size: 13px !important;
padding: 14px 16px !important;
line-height: 1.5 !important;
}
.pmpro-tooltip-trigger {
font-size: 18px !important;
width: 18px !important;
height: 18px !important;
margin-left: 6px !important;
}
}
/* ====================================================================
BREAKPOINT MÓVIL (480px)
==================================================================== */
@media (max-width: 480px) {
/* Reset completo para móviles */
body {
overflow-x: hidden !important;
}
/* Contenedores ultra compactos */
fieldset {
padding: 8px 5px !important;
margin: 0 !important;
}
fieldset legend {
font-size: 14px !important;
max-width: calc(100vw - 30px) !important;
padding: 0 5px !important;
word-break: break-word !important;
overflow-wrap: break-word !important;
}
.pmpro_checkout-field,
.pmpro_billing-field,
.pmpro_account-field {
margin: 8px 0 !important;
padding: 0 2px !important;
max-width: 100% !important;
overflow-x: hidden !important;
}
/* Selects móviles */
#storage_space,
#num_users,
#payment_frequency,
select#storage_space,
select#num_users,
select#payment_frequency {
padding: 8px 30px 8px 10px !important;
font-size: 13px !important;
background-size: 10px !important;
background-position: right 10px center !important;
min-height: 40px !important;
}
#num_users option,
#storage_space option,
#payment_frequency option {
font-size: 12px !important;
padding: 6px 10px !important;
min-height: 32px !important;
line-height: 1.3 !important;
word-break: break-word !important;
overflow-wrap: break-word !important;
}
/* Campo de precio móvil */
#total_price_display {
font-size: 16px !important;
padding: 8px !important;
text-align: center !important;
}
/* Sección bordered-field móvil */
.pmpro_checkout-field-office-suite.bordered-field {
padding: 8px !important;
margin: 10px 0 !important;
border-radius: 4px !important;
overflow-x: hidden !important;
max-width: 100% !important;
}
.pmpro_checkout-field-office-suite.bordered-field label {
font-size: 16px !important;
margin-bottom: 6px !important;
word-break: break-word !important;
overflow-wrap: break-word !important;
}
/* Tooltips móviles */
.pmpro-tooltip {
width: 300px !important;
max-width: calc(100vw - 10px) !important;
font-size: 12px !important;
padding: 12px 14px !important;
line-height: 1.4 !important;
left: 5px !important;
right: 5px !important;
}
.pmpro-tooltip-trigger {
font-size: 16px !important;
width: 16px !important;
height: 16px !important;
margin-left: 4px !important;
}
/* Labels más compactos */
.pmpro_checkout-field label,
.pmpro_billing-field label,
.pmpro_account-field label {
font-size: 13px !important;
line-height: 1.3 !important;
word-wrap: break-word !important;
overflow-wrap: break-word !important;
max-width: 100% !important;
margin-bottom: 6px !important;
}
/* Mensajes de alerta móviles */
.pmpro_message {
padding: 8px 10px !important;
margin: 10px 0 !important;
font-size: 12px !important;
box-sizing: border-box !important;
}
/* Profile fields móviles */
.pmpro_account-profile-field {
margin: 10px 0 !important;
padding: 10px !important;
}
}
/* ====================================================================
BREAKPOINT PANTALLAS MUY PEQUEÑAS (360px)
==================================================================== */
@media (max-width: 360px) {
/* Ultra compacto para pantallas pequeñas */
fieldset {
padding: 5px 3px !important;
}
fieldset legend {
font-size: 13px !important;
max-width: calc(100vw - 20px) !important;
padding: 0 3px !important;
}
.pmpro_checkout-field,
.pmpro_billing-field,
.pmpro_account-field {
padding: 0 1px !important;
margin: 6px 0 !important;
}
/* Selects ultra compactos */
#storage_space,
#num_users,
#payment_frequency,
select#storage_space,
select#num_users,
select#payment_frequency {
padding: 6px 25px 6px 8px !important;
font-size: 12px !important;
background-size: 8px !important;
background-position: right 8px center !important;
min-height: 36px !important;
}
#num_users option,
#storage_space option,
#payment_frequency option {
font-size: 11px !important;
padding: 4px 8px !important;
min-height: 28px !important;
line-height: 1.2 !important;
}
/* Campo de precio ultra compacto */
#total_price_display {
font-size: 14px !important;
padding: 6px !important;
}
/* Sección bordered-field ultra compacta */
.pmpro_checkout-field-office-suite.bordered-field {
padding: 6px !important;
margin: 8px 0 !important;
}
.pmpro_checkout-field-office-suite.bordered-field label {
font-size: 16px !important;
margin-bottom: 4px !important;
}
/* Tooltips ultra pequeños */
.pmpro-tooltip {
width: 280px !important;
max-width: calc(100vw - 5px) !important;
font-size: 11px !important;
padding: 10px 12px !important;
left: 2px !important;
right: 2px !important;
}
.pmpro-tooltip-trigger {
font-size: 14px !important;
width: 14px !important;
height: 14px !important;
}
/* Labels ultra compactos */
.pmpro_checkout-field label,
.pmpro_billing-field label,
.pmpro_account-field label {
font-size: 12px !important;
line-height: 1.2 !important;
margin-bottom: 4px !important;
}
}
/* ====================================================================
PREVENIR INTERFERENCIAS DE NAVEGADORES
==================================================================== */
/* Webkit */
select#num_users::-ms-expand,
select#storage_space::-ms-expand,
select#payment_frequency::-ms-expand {
display: none !important;
}
/* Internet Explorer */
select#num_users::-ms-dropdown,
select#storage_space::-ms-dropdown,
select#payment_frequency::-ms-dropdown {
display: none !important;
}
/* Firefox */
select#num_users:-moz-focusring,
select#storage_space:-moz-focusring,
select#payment_frequency:-moz-focusring {
color: transparent !important;
text-shadow: 0 0 0 #000 !important;
}
/* ====================================================================
ESTADOS ESPECIALES
==================================================================== */
/* Selects disabled */
#storage_space:disabled,
#num_users:disabled,
#payment_frequency:disabled {
background-color: #f5f5f5 !important;
color: #999 !important;
cursor: not-allowed !important;
background-image: url("data:image/svg+xml;charset=US-ASCII,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'><path fill='%23999' d='m0 0 2 2 2-2z'/></svg>") !important;
}
/* Estados de validación */
.pmpro_checkout-field.pmpro_error select {
border-color: #dc2626 !important;
box-shadow: 0 0 0 2px rgba(220, 38, 38, 0.1) !important;
}
.pmpro_checkout-field.pmpro_error select:focus {
border-color: #dc2626 !important;
box-shadow: 0 0 0 2px rgba(220, 38, 38, 0.2) !important;
}
/* ====================================================================
COMPATIBILIDAD CON TEMAS Y PAGE BUILDERS
==================================================================== */
/* Astra Theme */
.ast-container .pmpro_checkout-field,
.ast-container .pmpro_billing-field,
.ast-container .pmpro_account-field {
max-width: 100% !important;
overflow-x: hidden !important;
}
.ast-container select#num_users,
.ast-container select#storage_space,
.ast-container select#payment_frequency {
width: 100% !important;
max-width: 100% !important;
-webkit-appearance: none !important;
background-image: url("data:image/svg+xml;charset=US-ASCII,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'><path fill='%23666' d='m0 0 2 2 2-2z'/></svg>") !important;
}
/* Elementor */
.elementor .pmpro_checkout-field,
.elementor .pmpro_billing-field,
.elementor .pmpro_account-field,
.elementor-widget .pmpro_checkout-field {
max-width: 100% !important;
width: 100% !important;
box-sizing: border-box !important;
overflow-x: hidden !important;
}
.elementor select#num_users,
.elementor select#storage_space,
.elementor select#payment_frequency {
width: 100% !important;
max-width: 100% !important;
-webkit-appearance: none !important;
background-image: url("data:image/svg+xml;charset=US-ASCII,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'><path fill='%23666' d='m0 0 2 2 2-2z'/></svg>") !important;
}
/* GeneratePress */
.site-content .pmpro_checkout-field,
.entry-content .pmpro_checkout-field,
.main-content .pmpro_checkout-field {
max-width: 100% !important;
overflow-x: hidden !important;
}
.site-content select#num_users,
.site-content select#storage_space,
.site-content select#payment_frequency {
width: 100% !important;
max-width: 100% !important;
-webkit-appearance: none !important;
background-image: url("data:image/svg+xml;charset=US-ASCII,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'><path fill='%23666' d='m0 0 2 2 2-2z'/></svg>") !important;
}
/* Divi Theme */
.et_pb_row .pmpro_checkout-field,
.et_pb_column .pmpro_checkout-field {
max-width: 100% !important;
box-sizing: border-box !important;
overflow-x: hidden !important;
}
/* Avada Theme */
.fusion-builder-row .pmpro_checkout-field,
.fusion-layout-column .pmpro_checkout-field {
max-width: 100% !important;
box-sizing: border-box !important;
overflow-x: hidden !important;
}
/* ====================================================================
LIMPIAR ESTILOS CONFLICTIVOS
==================================================================== */
/* Remover pseudo-elementos que puedan crear flechas adicionales */
select#num_users::after,
select#num_users::before,
select#storage_space::after,
select#storage_space::before,
select#payment_frequency::after,
select#payment_frequency::before {
display: none !important;
content: none !important;
}
/* Background consistente */
select#num_users,
select#storage_space,
select#payment_frequency {
background-attachment: scroll !important;
background-clip: padding-box !important;
background-origin: padding-box !important;
}
/* ====================================================================
UTILITIES Y DEBUG
==================================================================== */
/* Clase utility para forzar responsive */
.pmpro-force-responsive {
max-width: 100% !important;
width: 100% !important;
box-sizing: border-box !important;
overflow-x: hidden !important;
}
/* Debug clase (remover en producción) */
.pmpro-debug-mobile {
outline: 2px solid red !important;
}
/* Prevenir zoom en iOS */
@media screen and (-webkit-min-device-pixel-ratio: 0) {
select#num_users,
select#storage_space,
select#payment_frequency {
font-size: 16px !important;
}
}
@media (max-width: 480px) {
/* Mantener 16px mínimo en iOS para prevenir zoom */
select#num_users,
select#storage_space,
select#payment_frequency {
font-size: 16px !important;
}
}
/* ====================================================================
ACCESIBILIDAD
==================================================================== */
/* Focus visible mejorado */
#storage_space:focus-visible,
#num_users:focus-visible,
#payment_frequency:focus-visible {
outline: 2px solid #2d9cdb !important;
outline-offset: 2px !important;
}
/* High contrast mode */
@media (prefers-contrast: high) {
.pmpro-tooltip {
background-color: #000 !important;
border: 2px solid #fff !important;
}
.pmpro_checkout-field-office-suite.bordered-field {
border-width: 2px !important;
border-color: #000 !important;
}
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
.pmpro-tooltip-trigger {
transition: none !important;
}
.pmpro-tooltip {
transition: none !important;
}
}
/* ====================================================================
ESTILOS PARA PRORRATEO (UPGRADES)
==================================================================== */
/* Estado de prorrateo en el campo de precio */
#total_price_display.prorated-price {
background-color: #fff8e1 !important;
border: 2px solid #ffd54f !important;
color: #8c6b08 !important;
font-weight: 700 !important;
}
/* Label para precio prorrateado */
.pmpro_checkout-field-price-display.prorated-label label {
color: #8c6b08 !important;
font-weight: 700 !important;
}
/* Mensaje de información de prorrateo */
.pmpro-prorated-notice {
background-color: #e8f5e8 !important;
border: 1px solid #4caf50 !important;
border-radius: 6px !important;
padding: 10px 16px !important;
margin: 10px 0 !important;
font-size: 14px !important;
line-height: 1.3 !important;
color: #26692a !important;
max-width: 100% !important;
box-sizing: border-box !important;
}
.pmpro-prorated-notice p {
margin: 0 0 8px 0 !important;
padding: 0 !important;
}
.pmpro-prorated-notice p:last-child {
margin-bottom: 0 !important;
}
/* Icono de información */
.pmpro-prorated-notice::before {
content: "";
margin-right: 8px;
font-size: 16px;
}
/* Estilos para precio prorrateado */
.prorated-price {
color: #b33; /* rojo oscuro para resaltar */
font-weight: 700;
transition: color 0.15s;
}
.pmpro-prorated-notice {
border-left: 4px solid #b33;
background: #fff5f5;
padding: 10px;
margin-top: 8px;
border-radius: 4px;
color: #333;
font-size: 0.95em;
}
.prorated-label {
color: #b33;
font-weight: 600;
}
/* Responsive para mensajes de prorrateo */
@media (max-width: 768px) {
.pmpro-prorated-notice {
padding: 10px 12px !important;
font-size: 13px !important;
}
}
@media (max-width: 480px) {
.pmpro-prorated-notice {
padding: 8px 10px !important;
font-size: 12px !important;
}
}

View File

@ -0,0 +1,824 @@
/* Dynamic pricing for PMPro CSS - VERSIÓN COMPLETA OPTIMIZADA v2.1 */
/* ====================================================================
RESET Y BASE
==================================================================== */
/* Prevenir desbordamiento horizontal global */
* {
box-sizing: border-box !important;
}
body {
overflow-x: hidden !important;
}
/* ====================================================================
ESTILOS GENERALES DEL FORMULARIO
==================================================================== */
#total_price_display {
background-color: #edf5f9 !important;
border: none !important;
font-size: 20px !important;
font-weight: 600 !important;
cursor: default !important;
pointer-events: none !important;
color: #2d5a87 !important;
text-align: center !important;
max-width: 100% !important;
width: 100% !important;
padding: 12px !important;
border-radius: 6px !important;
box-sizing: border-box !important;
}
.pmpro_account-profile-field {
margin: 20px 0 !important;
padding: 15px !important;
background: #f8f8f8 !important;
border-radius: 5px !important;
border-left: 4px solid #2d9cdb !important;
max-width: 100% !important;
box-sizing: border-box !important;
overflow-x: hidden !important;
}
.pmpro_nextcloud_config_details p {
margin: 8px 0 !important;
line-height: 1.4 !important;
word-wrap: break-word !important;
overflow-wrap: break-word !important;
}
.pmpro_nextcloud_config_details h3 {
color: #2d5a87 !important;
margin-bottom: 15px !important;
border-bottom: 2px solid #e0e0e0 !important;
padding-bottom: 8px !important;
word-wrap: break-word !important;
overflow-wrap: break-word !important;
}
/* ====================================================================
CONTENEDORES PRINCIPALES - CORRECCIÓN PARA MÓVILES
==================================================================== */
/* Contenedor principal del formulario */
#pmpro_checkout,
.pmpro_checkout,
.pmpro_billing,
.pmpro_account {
max-width: 100% !important;
overflow-x: hidden !important;
box-sizing: border-box !important;
}
/* Fieldsets responsive */
fieldset {
max-width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
overflow-x: hidden !important;
}
fieldset legend {
max-width: calc(100% - 20px) !important;
padding: 0 10px !important;
font-size: 16px !important;
font-weight: 600 !important;
word-wrap: break-word !important;
overflow-wrap: break-word !important;
box-sizing: border-box !important;
}
/* Campos individuales */
.pmpro_checkout-field,
.pmpro_billing-field,
.pmpro_account-field {
max-width: 100% !important;
box-sizing: border-box !important;
overflow-x: hidden !important;
margin: 15px 0 !important;
padding: 0 !important;
}
/* ====================================================================
SELECTS - OPTIMIZADOS PARA MÓVILES
==================================================================== */
/* Base para todos los selects con flecha única */
#storage_space,
#office_suite,
#payment_frequency,
select#office_suite,
select#storage_space,
select#payment_frequency {
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
padding: 12px 40px 12px 15px !important;
border: 1px solid #ddd !important;
border-radius: 6px !important;
font-size: 14px !important;
line-height: 1.4 !important;
transition: border-color 0.3s ease !important;
box-sizing: border-box !important;
/* Remover estilos nativos */
-webkit-appearance: none !important;
-moz-appearance: none !important;
-ms-appearance: none !important;
appearance: none !important;
/* Flecha personalizada única */
background-color: white !important;
background-image: url("data:image/svg+xml;charset=US-ASCII,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'><path fill='%23666' d='m0 0 2 2 2-2z'/></svg>") !important;
background-repeat: no-repeat !important;
background-position: right 15px center !important;
background-size: 12px !important;
}
/* Selectores con máxima especificidad */
html body .pmpro_checkout-field select,
html body .pmpro_billing-field select,
html body .pmpro_account-field select,
html body #office_suite,
html body #storage_space,
html body #payment_frequency,
body .pmpro_checkout-field select#office_suite,
body .pmpro_billing-field select#office_suite,
body .pmpro_account-field select#office_suite {
-webkit-appearance: none !important;
-moz-appearance: none !important;
-ms-appearance: none !important;
appearance: none !important;
background-image: url("data:image/svg+xml;charset=US-ASCII,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'><path fill='%23666' d='m0 0 2 2 2-2z'/></svg>") !important;
background-repeat: no-repeat !important;
background-position: right 15px center !important;
background-size: 12px !important;
background-color: #cadbe6 !important;
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
border: 1px solid #777 !important;
}
/* Estados de focus */
#storage_space:focus,
#office_suite:focus,
#payment_frequency:focus {
border-color: #2d9cdb !important;
outline: none !important;
box-shadow: 0 0 0 2px rgba(45, 156, 219, 0.1) !important;
}
/* ====================================================================
OPCIONES DEL SELECT
==================================================================== */
#office_suite option,
#storage_space option,
#payment_frequency option {
padding: 10px 15px !important;
white-space: normal !important;
overflow: visible !important;
min-height: 40px !important;
line-height: 1.4 !important;
word-wrap: break-word !important;
overflow-wrap: break-word !important;
-webkit-text-size-adjust: none !important;
-moz-text-size-adjust: none !important;
-ms-text-size-adjust: none !important;
text-size-adjust: none !important;
}
/* Opciones deshabilitadas */
#storage_space option:disabled,
#office_suite option:disabled,
#payment_frequency option:disabled {
color: #999 !important;
background-color: #f5f5f5 !important;
}
/* ====================================================================
CAMPO DE SUITE OFIMÁTICA CON BORDE
==================================================================== */
.pmpro_checkout-field-office-suite.bordered-field {
border: 1px solid #2d9cdb !important;
border-radius: 8px !important;
padding: 16px !important;
background-color: #f9fbff !important;
margin: 20px 0 !important;
position: relative !important;
overflow-x: hidden !important;
max-width: 100% !important;
box-sizing: border-box !important;
}
.pmpro_checkout-field-office-suite.bordered-field label {
font-weight: 600 !important;
color: #2d5a87 !important;
display: block !important;
position: relative !important;
word-wrap: break-word !important;
overflow-wrap: break-word !important;
max-width: 100% !important;
box-sizing: border-box !important;
}
/* ====================================================================
SISTEMA DE TOOLTIPS
==================================================================== */
.pmpro-tooltip-trigger {
color: #2d9cdb !important;
cursor: help !important;
font-size: 22px !important;
vertical-align: top !important;
display: inline-block !important;
width: 20px !important;
height: 20px !important;
transition: all 0.2s ease !important;
position: relative !important;
z-index: 1 !important;
}
.pmpro-tooltip-trigger:hover {
color: #1e6091 !important;
transform: scale(1.1) !important;
}
.pmpro-tooltip {
position: absolute !important;
background-color: #333 !important;
color: white !important;
padding: 16px 20px !important;
border-radius: 8px !important;
font-size: 13px !important;
line-height: 1.1 !important;
width: 400px !important;
max-width: 95vw !important;
z-index: 10001 !important;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2) !important;
word-wrap: break-word !important;
word-break: normal !important;
overflow-wrap: break-word !important;
white-space: normal !important;
text-align: left !important;
box-sizing: border-box !important;
pointer-events: auto !important;
transition: opacity 0.3s ease, visibility 0.3s ease !important;
}
.pmpro-tooltip p {
margin: 0 0 12px 0 !important;
padding: 0 !important;
}
.pmpro-tooltip p:last-child {
margin-bottom: 0 !important;
}
.pmpro-tooltip strong {
color: #fff !important;
font-weight: 600 !important;
display: inline !important;
}
/* ====================================================================
MENSAJES DE ALERTA
==================================================================== */
.pmpro_message {
padding: 12px 16px !important;
margin: 16px 0 !important;
border-radius: 6px !important;
font-size: 14px !important;
max-width: 100% !important;
box-sizing: border-box !important;
word-wrap: break-word !important;
overflow-wrap: break-word !important;
}
.pmpro_message.pmpro_error {
background-color: #fdf2f2 !important;
border: 1px solid #fecaca !important;
color: #dc2626 !important;
}
/* ====================================================================
BREAKPOINT TABLET (768px)
==================================================================== */
@media (max-width: 768px) {
/* Contenedor principal más compacto */
fieldset {
padding: 12px 8px !important;
}
fieldset legend {
font-size: 15px !important;
max-width: calc(100% - 16px) !important;
padding: 0 8px !important;
}
.pmpro_checkout-field,
.pmpro_billing-field,
.pmpro_account-field {
margin: 12px 0 !important;
padding: 0 5px !important;
}
/* Selects responsive */
#storage_space,
#office_suite,
#payment_frequency,
select#storage_space,
select#office_suite,
select#payment_frequency {
padding: 10px 35px 10px 12px !important;
font-size: 14px !important;
background-size: 11px !important;
background-position: right 12px center !important;
}
#office_suite option,
#storage_space option,
#payment_frequency option {
font-size: 13px !important;
padding: 8px 12px !important;
min-height: 35px !important;
line-height: 1.3 !important;
}
/* Campo de precio responsive */
#total_price_display {
font-size: 18px !important;
padding: 10px !important;
}
/* Sección bordered-field responsive */
.pmpro_checkout-field-office-suite.bordered-field {
padding: 12px !important;
margin: 15px 0 !important;
border-radius: 6px !important;
}
.pmpro_checkout-field-office-suite.bordered-field label {
font-size: 16px !important;
margin-bottom: 8px !important;
}
/* Tooltips responsive */
.pmpro-tooltip {
width: 350px !important;
max-width: calc(100vw - 20px) !important;
font-size: 13px !important;
padding: 14px 16px !important;
line-height: 1.5 !important;
}
.pmpro-tooltip-trigger {
font-size: 18px !important;
width: 18px !important;
height: 18px !important;
margin-left: 6px !important;
}
}
/* ====================================================================
BREAKPOINT MÓVIL (480px)
==================================================================== */
@media (max-width: 480px) {
/* Reset completo para móviles */
body {
overflow-x: hidden !important;
}
/* Contenedores ultra compactos */
fieldset {
padding: 8px 5px !important;
margin: 0 !important;
}
fieldset legend {
font-size: 14px !important;
max-width: calc(100vw - 30px) !important;
padding: 0 5px !important;
word-break: break-word !important;
overflow-wrap: break-word !important;
}
.pmpro_checkout-field,
.pmpro_billing-field,
.pmpro_account-field {
margin: 8px 0 !important;
padding: 0 2px !important;
max-width: 100% !important;
overflow-x: hidden !important;
}
/* Selects móviles */
#storage_space,
#office_suite,
#payment_frequency,
select#storage_space,
select#office_suite,
select#payment_frequency {
padding: 8px 30px 8px 10px !important;
font-size: 13px !important;
background-size: 10px !important;
background-position: right 10px center !important;
min-height: 40px !important;
}
#office_suite option,
#storage_space option,
#payment_frequency option {
font-size: 12px !important;
padding: 6px 10px !important;
min-height: 32px !important;
line-height: 1.3 !important;
word-break: break-word !important;
overflow-wrap: break-word !important;
}
/* Campo de precio móvil */
#total_price_display {
font-size: 16px !important;
padding: 8px !important;
text-align: center !important;
}
/* Sección bordered-field móvil */
.pmpro_checkout-field-office-suite.bordered-field {
padding: 8px !important;
margin: 10px 0 !important;
border-radius: 4px !important;
overflow-x: hidden !important;
max-width: 100% !important;
}
.pmpro_checkout-field-office-suite.bordered-field label {
font-size: 16px !important;
margin-bottom: 6px !important;
word-break: break-word !important;
overflow-wrap: break-word !important;
}
/* Tooltips móviles */
.pmpro-tooltip {
width: 300px !important;
max-width: calc(100vw - 10px) !important;
font-size: 12px !important;
padding: 12px 14px !important;
line-height: 1.4 !important;
left: 5px !important;
right: 5px !important;
}
.pmpro-tooltip-trigger {
font-size: 16px !important;
width: 16px !important;
height: 16px !important;
margin-left: 4px !important;
}
/* Labels más compactos */
.pmpro_checkout-field label,
.pmpro_billing-field label,
.pmpro_account-field label {
font-size: 13px !important;
line-height: 1.3 !important;
word-wrap: break-word !important;
overflow-wrap: break-word !important;
max-width: 100% !important;
margin-bottom: 6px !important;
}
/* Mensajes de alerta móviles */
.pmpro_message {
padding: 8px 10px !important;
margin: 10px 0 !important;
font-size: 12px !important;
box-sizing: border-box !important;
}
/* Profile fields móviles */
.pmpro_account-profile-field {
margin: 10px 0 !important;
padding: 10px !important;
}
}
/* ====================================================================
BREAKPOINT PANTALLAS MUY PEQUEÑAS (360px)
==================================================================== */
@media (max-width: 360px) {
/* Ultra compacto para pantallas pequeñas */
fieldset {
padding: 5px 3px !important;
}
fieldset legend {
font-size: 13px !important;
max-width: calc(100vw - 20px) !important;
padding: 0 3px !important;
}
.pmpro_checkout-field,
.pmpro_billing-field,
.pmpro_account-field {
padding: 0 1px !important;
margin: 6px 0 !important;
}
/* Selects ultra compactos */
#storage_space,
#office_suite,
#payment_frequency,
select#storage_space,
select#office_suite,
select#payment_frequency {
padding: 6px 25px 6px 8px !important;
font-size: 12px !important;
background-size: 8px !important;
background-position: right 8px center !important;
min-height: 36px !important;
}
#office_suite option,
#storage_space option,
#payment_frequency option {
font-size: 11px !important;
padding: 4px 8px !important;
min-height: 28px !important;
line-height: 1.2 !important;
}
/* Campo de precio ultra compacto */
#total_price_display {
font-size: 14px !important;
padding: 6px !important;
}
/* Sección bordered-field ultra compacta */
.pmpro_checkout-field-office-suite.bordered-field {
padding: 6px !important;
margin: 8px 0 !important;
}
.pmpro_checkout-field-office-suite.bordered-field label {
font-size: 16px !important;
margin-bottom: 4px !important;
}
/* Tooltips ultra pequeños */
.pmpro-tooltip {
width: 280px !important;
max-width: calc(100vw - 5px) !important;
font-size: 11px !important;
padding: 10px 12px !important;
left: 2px !important;
right: 2px !important;
}
.pmpro-tooltip-trigger {
font-size: 14px !important;
width: 14px !important;
height: 14px !important;
}
/* Labels ultra compactos */
.pmpro_checkout-field label,
.pmpro_billing-field label,
.pmpro_account-field label {
font-size: 12px !important;
line-height: 1.2 !important;
margin-bottom: 4px !important;
}
}
/* ====================================================================
PREVENIR INTERFERENCIAS DE NAVEGADORES
==================================================================== */
/* Webkit */
select#office_suite::-ms-expand,
select#storage_space::-ms-expand,
select#payment_frequency::-ms-expand {
display: none !important;
}
/* Internet Explorer */
select#office_suite::-ms-dropdown,
select#storage_space::-ms-dropdown,
select#payment_frequency::-ms-dropdown {
display: none !important;
}
/* Firefox */
select#office_suite:-moz-focusring,
select#storage_space:-moz-focusring,
select#payment_frequency:-moz-focusring {
color: transparent !important;
text-shadow: 0 0 0 #000 !important;
}
/* ====================================================================
ESTADOS ESPECIALES
==================================================================== */
/* Selects disabled */
#storage_space:disabled,
#office_suite:disabled,
#payment_frequency:disabled {
background-color: #f5f5f5 !important;
color: #999 !important;
cursor: not-allowed !important;
background-image: url("data:image/svg+xml;charset=US-ASCII,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'><path fill='%23999' d='m0 0 2 2 2-2z'/></svg>") !important;
}
/* Estados de validación */
.pmpro_checkout-field.pmpro_error select {
border-color: #dc2626 !important;
box-shadow: 0 0 0 2px rgba(220, 38, 38, 0.1) !important;
}
.pmpro_checkout-field.pmpro_error select:focus {
border-color: #dc2626 !important;
box-shadow: 0 0 0 2px rgba(220, 38, 38, 0.2) !important;
}
/* ====================================================================
COMPATIBILIDAD CON TEMAS Y PAGE BUILDERS
==================================================================== */
/* Astra Theme */
.ast-container .pmpro_checkout-field,
.ast-container .pmpro_billing-field,
.ast-container .pmpro_account-field {
max-width: 100% !important;
overflow-x: hidden !important;
}
.ast-container select#office_suite,
.ast-container select#storage_space,
.ast-container select#payment_frequency {
width: 100% !important;
max-width: 100% !important;
-webkit-appearance: none !important;
background-image: url("data:image/svg+xml;charset=US-ASCII,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'><path fill='%23666' d='m0 0 2 2 2-2z'/></svg>") !important;
}
/* Elementor */
.elementor .pmpro_checkout-field,
.elementor .pmpro_billing-field,
.elementor .pmpro_account-field,
.elementor-widget .pmpro_checkout-field {
max-width: 100% !important;
width: 100% !important;
box-sizing: border-box !important;
overflow-x: hidden !important;
}
.elementor select#office_suite,
.elementor select#storage_space,
.elementor select#payment_frequency {
width: 100% !important;
max-width: 100% !important;
-webkit-appearance: none !important;
background-image: url("data:image/svg+xml;charset=US-ASCII,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'><path fill='%23666' d='m0 0 2 2 2-2z'/></svg>") !important;
}
/* GeneratePress */
.site-content .pmpro_checkout-field,
.entry-content .pmpro_checkout-field,
.main-content .pmpro_checkout-field {
max-width: 100% !important;
overflow-x: hidden !important;
}
.site-content select#office_suite,
.site-content select#storage_space,
.site-content select#payment_frequency {
width: 100% !important;
max-width: 100% !important;
-webkit-appearance: none !important;
background-image: url("data:image/svg+xml;charset=US-ASCII,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'><path fill='%23666' d='m0 0 2 2 2-2z'/></svg>") !important;
}
/* Divi Theme */
.et_pb_row .pmpro_checkout-field,
.et_pb_column .pmpro_checkout-field {
max-width: 100% !important;
box-sizing: border-box !important;
overflow-x: hidden !important;
}
/* Avada Theme */
.fusion-builder-row .pmpro_checkout-field,
.fusion-layout-column .pmpro_checkout-field {
max-width: 100% !important;
box-sizing: border-box !important;
overflow-x: hidden !important;
}
/* ====================================================================
LIMPIAR ESTILOS CONFLICTIVOS
==================================================================== */
/* Remover pseudo-elementos que puedan crear flechas adicionales */
select#office_suite::after,
select#office_suite::before,
select#storage_space::after,
select#storage_space::before,
select#payment_frequency::after,
select#payment_frequency::before {
display: none !important;
content: none !important;
}
/* Background consistente */
select#office_suite,
select#storage_space,
select#payment_frequency {
background-attachment: scroll !important;
background-clip: padding-box !important;
background-origin: padding-box !important;
}
/* ====================================================================
UTILITIES Y DEBUG
==================================================================== */
/* Clase utility para forzar responsive */
.pmpro-force-responsive {
max-width: 100% !important;
width: 100% !important;
box-sizing: border-box !important;
overflow-x: hidden !important;
}
/* Debug clase (remover en producción) */
.pmpro-debug-mobile {
outline: 2px solid red !important;
}
/* Prevenir zoom en iOS */
@media screen and (-webkit-min-device-pixel-ratio: 0) {
select#office_suite,
select#storage_space,
select#payment_frequency {
font-size: 16px !important;
}
}
@media (max-width: 480px) {
/* Mantener 16px mínimo en iOS para prevenir zoom */
select#office_suite,
select#storage_space,
select#payment_frequency {
font-size: 16px !important;
}
}
/* ====================================================================
ACCESIBILIDAD
==================================================================== */
/* Focus visible mejorado */
#storage_space:focus-visible,
#office_suite:focus-visible,
#payment_frequency:focus-visible {
outline: 2px solid #2d9cdb !important;
outline-offset: 2px !important;
}
/* High contrast mode */
@media (prefers-contrast: high) {
.pmpro-tooltip {
background-color: #000 !important;
border: 2px solid #fff !important;
}
.pmpro_checkout-field-office-suite.bordered-field {
border-width: 2px !important;
border-color: #000 !important;
}
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
.pmpro-tooltip-trigger {
transition: none !important;
}
.pmpro-tooltip {
transition: none !important;
}
}

View File

View File

View File

@ -0,0 +1,127 @@
.simple-accordion {
border-radius: 0;
overflow: hidden;
}
.simple-accordion .accordion-item {
border: 1px solid #999;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin-bottom: 5px;
}
.simple-accordion .accordion-item:last-child {
border-bottom: none;
}
.simple-accordion .accordion-header {
cursor: pointer;
padding: 5px 20px;
background: #e5e5e5;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
transition: all 0.3s ease;
user-select: none;
}
.simple-accordion .accordion-header.active {
border-bottom: 1px solid #a8a8a8;
}
.simple-accordion .accordion-header:focus {
outline: none;
}
.simple-accordion .accordion-header:hover {
background: #d9d9d9;
}
.simple-accordion .accordion-header.active {
background: #d9d9d9;
color: #000;
}
.simple-accordion .accordion-header.active:focus {
outline: none;
}
.simple-accordion .accordion-title-wrapper {
display: flex;
align-items: center;
gap: 12px;
}
.simple-accordion .accordion-icon-left {
font-size: 20px;
width: 20px;
height: 20px;
color: #555;
transition: color 0.3s ease;
}
.simple-accordion .accordion-header.active .accordion-icon-left {
color: #555;
}
.simple-accordion .accordion-title {
font-size: 1.2em;
font-weight: 600;
line-height: 1.2;
color: #000;
}
.simple-accordion .accordion-icon {
font-size: 1.5em;
font-weight: bold;
transition: transform 0.3s ease;
min-width: 30px;
text-align: center;
color: #555;
}
.simple-accordion .accordion-header.active .accordion-icon {
transform: rotate(45deg);
color: #555;
}
.simple-accordion .accordion-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.4s ease-in-out;
background: #f1f1f1;
}
.simple-accordion .accordion-content.active {
max-height: 5000px;
}
.simple-accordion .accordion-body {
padding: 2px 12px;
border-top: 1px solid #e0e0e0;
}
/* Responsive */
@media (max-width: 768px) {
.simple-accordion .accordion-header {
padding: 5px 20px;
}
.simple-accordion .accordion-body {
padding: 2px 12px;
}
.simple-accordion .accordion-title-wrapper {
gap: 10px;
}
.simple-accordion .accordion-icon-left {
font-size: 20px;
width: 18px;
height: 18px;
}
.simple-accordion .accordion-title {
font-size: 1em;
}
}

View File

View File

@ -0,0 +1 @@
/* Tu código CSS aquí */

View File

View File

@ -0,0 +1,14 @@
// Back to Top Button JS
document.addEventListener("DOMContentLoaded", function(event) {
var offset = 500, duration = 400;
var toTopButton = document.getElementById('toTop');
window.addEventListener('scroll', function() {
(window.pageYOffset > offset) ? toTopButton.style.display = 'block' : toTopButton.style.display = 'none';
});
toTopButton.addEventListener('click', function(event) {
event.preventDefault();
window.scroll({ top: 0, left: 0, behavior: 'smooth' });
});
});

View File

View File

View File

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,660 @@
/**
* PMPro Dynamic Pricing - Frontend JavaScript Optimizado v2.1
* Estructura de snippet con optimizaciones de rendimiento
*
* @version 2.1.0
*/
jQuery(document).ready(function($) {
'use strict';
// ====================================================================
// CONFIGURACIÓN Y VARIABLES GLOBALES
// ====================================================================
var PLUGIN_VERSION = '2.1.0';
var DEBUG_MODE = typeof console !== 'undefined' && (typeof nextcloud_pricing !== 'undefined' && nextcloud_pricing.debug);
var CACHE_EXPIRY = 60000; // 1 minuto
var TOOLTIP_DELAY = 300;
// Cache simple para optimización
var priceCache = {};
var originalTextsCache = {};
var activeTooltip = null;
var showTooltipTimer = null;
var hideTooltipTimer = null;
// ====================================================================
// SISTEMA DE LOGGING
// ====================================================================
function log(level, message, data) {
if (!DEBUG_MODE) return;
var prefix = '[PMPro Dynamic ' + level + ']';
if (data && typeof data === 'object') {
console.log(prefix, message, data);
} else {
console.log(prefix, message);
}
}
function logError(message, data) { log('ERROR', message, data); }
function logWarn(message, data) { log('WARN', message, data); }
function logInfo(message, data) { log('INFO', message, data); }
function logDebug(message, data) { log('DEBUG', message, data); }
// ====================================================================
// VALIDACIÓN DE DEPENDENCIAS
// ====================================================================
function validateDependencies() {
var checks = {
nextcloud_pricing: typeof nextcloud_pricing !== 'undefined',
jquery: typeof $ !== 'undefined',
required_elements: $('#storage_space, #office_suite, #payment_frequency').length >= 3
};
var missing = [];
for (var check in checks) {
if (!checks[check]) {
missing.push(check);
}
}
if (missing.length > 0) {
logError('Missing dependencies', { missing: missing });
return false;
}
logInfo('Dependencies validated successfully');
return true;
}
// ====================================================================
// CONFIGURACIÓN DINÁMICA
// ====================================================================
function getConfig() {
if (typeof nextcloud_pricing === 'undefined') {
logError('nextcloud_pricing configuration not available');
return null;
}
return {
levelId: nextcloud_pricing.level_id || 1,
basePrice: parseInt(nextcloud_pricing.base_price) || 0,
currencySymbol: nextcloud_pricing.currency_symbol || 'R$',
currentStorage: nextcloud_pricing.current_storage || '1tb',
currentSuite: nextcloud_pricing.current_suite || '20users',
usedSpaceTb: parseFloat(nextcloud_pricing.used_space_tb) || 0,
pricePerTb: 120,
officeUserPrice: 25,
frequencyMultipliers: {
'monthly': 1.0,
'semiannual': 5.7,
'annual': 10.8,
'biennial': 20.4,
'triennial': 28.8,
'quadrennial': 36.0,
'quinquennial': 42.0
}
};
}
// ====================================================================
// SISTEMA DE CACHÉ
// ====================================================================
function getCachedPrice(key) {
var cached = priceCache[key];
if (cached && (Date.now() - cached.timestamp) < CACHE_EXPIRY) {
logDebug('Cache hit for price', { key: key });
return cached.value;
}
return null;
}
function setCachedPrice(key, value) {
priceCache[key] = {
value: value,
timestamp: Date.now()
};
logDebug('Price cached', { key: key });
}
function clearPriceCache() {
priceCache = {};
logDebug('Price cache cleared');
}
// ====================================================================
// SISTEMA DE TOOLTIPS OPTIMIZADO
// ====================================================================
var tooltipContent = {
'office-suite-tooltip': '<p>Integração do Collabora Online com o Nextcloud.</p>' +
'<p><strong>Collabora Online Development Edition (CODE)</strong> suporta uma média de 20 usuários simultaneos.</p>' +
'<p>Para um número maior de usuários, e para evitar instabilidade ou perda de documentos, você deve selecionar uma licença:</p>' +
'<p><strong>• Collabora Online for Business</strong> (até 99 usuários)<br>' +
'<strong>• Collabora Online for Enterprise</strong> (≥ 100 usuários)</p>' +
'<p><strong>ATENÇÃO:</strong> O número de usuários suportados pelo Collabora Online não limita o número de usuários suportados pelo Nextcloud.</p>'
};
function createTooltip() {
if (activeTooltip && activeTooltip.length) {
activeTooltip.remove();
}
activeTooltip = $('<div class="pmpro-tooltip" role="tooltip"></div>').css({
position: 'absolute',
background: '#333',
color: 'white',
padding: '16px 20px',
borderRadius: '8px',
fontSize: '14px',
lineHeight: '1.6',
width: '420px',
maxWidth: '95vw',
zIndex: 10001,
opacity: 0,
visibility: 'hidden',
transition: 'opacity 0.3s ease, visibility 0.3s ease',
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.2)',
wordWrap: 'break-word',
whiteSpace: 'normal',
textAlign: 'left',
boxSizing: 'border-box'
});
$('body').append(activeTooltip);
return activeTooltip;
}
function positionTooltip(tooltip, trigger) {
var triggerOffset = trigger.offset();
var triggerWidth = trigger.outerWidth();
var triggerHeight = trigger.outerHeight();
var windowWidth = $(window).width();
var windowHeight = $(window).height();
var scrollTop = $(window).scrollTop();
// Mostrar temporalmente para obtener dimensiones
tooltip.css({ visibility: 'visible', opacity: 0 });
var tooltipWidth = tooltip.outerWidth();
var tooltipHeight = tooltip.outerHeight();
// Calcular posición horizontal
var left = triggerOffset.left + (triggerWidth / 2) - (tooltipWidth / 2);
// Ajustar si se sale de la pantalla
if (left + tooltipWidth > windowWidth - 20) {
left = windowWidth - tooltipWidth - 20;
}
if (left < 20) {
left = 20;
}
// Calcular posición vertical
var top = triggerOffset.top - tooltipHeight - 12;
// Si no cabe arriba, mostrarlo abajo
if (top < scrollTop + 20) {
top = triggerOffset.top + triggerHeight + 12;
}
// Aplicar posición
tooltip.css({
left: left + 'px',
top: top + 'px',
visibility: 'visible',
opacity: 1
});
logDebug('Tooltip positioned', { left: left, top: top });
}
function showTooltip(trigger, contentKey) {
clearTimeout(hideTooltipTimer);
if (showTooltipTimer) {
clearTimeout(showTooltipTimer);
}
showTooltipTimer = setTimeout(function() {
var content = tooltipContent[contentKey];
if (!content) {
logWarn('No content found for tooltip', { contentKey: contentKey });
return;
}
var tooltip = createTooltip();
tooltip.html(content);
positionTooltip(tooltip, trigger);
logDebug('Tooltip shown', { contentKey: contentKey });
}, TOOLTIP_DELAY);
}
function hideTooltip() {
clearTimeout(showTooltipTimer);
if (hideTooltipTimer) {
clearTimeout(hideTooltipTimer);
}
hideTooltipTimer = setTimeout(function() {
if (activeTooltip && activeTooltip.length) {
activeTooltip.css({ opacity: 0, visibility: 'hidden' });
setTimeout(function() {
if (activeTooltip && activeTooltip.length) {
activeTooltip.remove();
activeTooltip = null;
}
}, 300);
}
logDebug('Tooltip hidden');
}, 200);
}
function initTooltips() {
logDebug('Initializing tooltip system');
// Limpiar eventos previos
$(document).off('mouseenter.tooltip mouseleave.tooltip keydown.tooltip');
$(window).off('resize.tooltip scroll.tooltip');
// Eventos de tooltip
$(document).on('mouseenter.tooltip', '.pmpro-tooltip-trigger', function(e) {
var $trigger = $(e.currentTarget);
var tooltipId = $trigger.data('tooltip-id');
if (tooltipId && tooltipContent[tooltipId]) {
showTooltip($trigger, tooltipId);
}
});
$(document).on('mouseleave.tooltip', '.pmpro-tooltip-trigger', function() {
hideTooltip();
});
// Cerrar con Escape
$(document).on('keydown.tooltip', function(e) {
if (e.key === 'Escape' || e.keyCode === 27) {
hideTooltip();
}
});
// Reposicionar en scroll/resize
$(window).on('resize.tooltip scroll.tooltip', function() {
if (activeTooltip && activeTooltip.css('opacity') == '1') {
hideTooltip();
}
});
var triggerCount = $('.pmpro-tooltip-trigger').length;
logInfo('Tooltip system initialized', { triggerCount: triggerCount });
}
// ====================================================================
// GESTIÓN DE PRECIOS
// ====================================================================
function getStoragePrices(config) {
var prices = {};
var options = ['1tb', '2tb', '3tb', '4tb', '5tb', '6tb', '7tb', '8tb', '9tb', '10tb',
'15tb', '20tb', '30tb', '40tb', '50tb', '60tb', '70tb', '80tb', '90tb', '100tb',
'200tb', '300tb', '400tb', '500tb'];
for (var i = 0; i < options.length; i++) {
var option = options[i];
var tb = parseInt(option.replace('tb', ''));
prices[option] = config.basePrice + (config.pricePerTb * Math.max(0, tb - 1));
}
return prices;
}
function getOfficePrices(config) {
return {
'20users': 0,
'30users': config.officeUserPrice * 30,
'50users': config.officeUserPrice * 50,
'80users': config.officeUserPrice * 80,
'100users': (config.officeUserPrice - 3.75) * 100,
'150users': (config.officeUserPrice - 3.75) * 150,
'200users': (config.officeUserPrice - 3.75) * 200,
'300users': (config.officeUserPrice - 3.75) * 300,
'400users': (config.officeUserPrice - 3.75) * 400,
'500users': (config.officeUserPrice - 3.75) * 500
};
}
function formatPrice(price, currencySymbol) {
var formatted = Math.ceil(price).toFixed(2)
.replace('.', ',')
.replace(/(\d)(?=(\d{3})+\,)/g, '$1.');
return currencySymbol + ' ' + formatted;
}
function getPeriodText(frequency) {
var periods = {
'monthly': ' (por mês)',
'semiannual': ' (por 6 meses)',
'annual': ' (por ano)',
'biennial': ' (por 2 anos)',
'triennial': ' (por 3 anos)',
'quadrennial': ' (por 4 anos)',
'quinquennial': ' (por 5 anos)'
};
return periods[frequency] || '';
}
function calculateTotalPrice(config) {
var storageValue = $('#storage_space').val() || config.currentStorage;
var officeValue = $('#office_suite').val() || config.currentSuite;
var frequencyValue = $('#payment_frequency').val() || 'monthly';
// Verificar caché
var cacheKey = storageValue + '_' + officeValue + '_' + frequencyValue + '_' + config.basePrice;
var cached = getCachedPrice(cacheKey);
if (cached !== null) {
return cached;
}
var storagePrices = getStoragePrices(config);
var officePrices = getOfficePrices(config);
var storagePrice = storagePrices[storageValue] || config.basePrice;
var officePrice = officePrices[officeValue] || 0;
var multiplier = config.frequencyMultipliers[frequencyValue] || 1.0;
var calculation = {
storageValue: storageValue,
officeValue: officeValue,
frequencyValue: frequencyValue,
storagePrice: storagePrice,
officePrice: officePrice,
multiplier: multiplier,
totalPrice: Math.ceil((storagePrice + officePrice) * multiplier)
};
setCachedPrice(cacheKey, calculation);
logDebug('Price calculated', calculation);
return calculation;
}
function updatePriceDisplay(config) {
try {
var calculation = calculateTotalPrice(config);
var formattedPrice = formatPrice(calculation.totalPrice, config.currencySymbol);
var periodText = getPeriodText(calculation.frequencyValue);
var displayText = formattedPrice + periodText;
var $display = $('#total_price_display');
if ($display.length) {
$display.val(displayText);
logDebug('Price display updated', { displayText: displayText });
}
// Trigger evento personalizado
$(document).trigger('pmpropricing:updated', [calculation]);
} catch (error) {
logError('Error updating price display', { error: error.message });
}
}
// ====================================================================
// GESTIÓN DE OPCIONES
// ====================================================================
function storeOriginalTexts() {
$('#office_suite option, #storage_space option, #payment_frequency option').each(function() {
var $option = $(this);
var selectId = $option.closest('select').attr('id');
var key = selectId + '_' + $option.val();
originalTextsCache[key] = $option.text();
});
logDebug('Original texts stored', { count: Object.keys(originalTextsCache).length });
}
function updateStorageOptions(config) {
var currentTb = parseInt(config.currentStorage.replace('tb', '')) || 1;
var $storageSelect = $('#storage_space');
if (!$storageSelect.length) return;
$storageSelect.find('option').each(function() {
var $option = $(this);
var optionTb = parseInt($option.val().replace('tb', ''));
var originalKey = 'storage_space_' + $option.val();
var originalText = originalTextsCache[originalKey] || $option.text();
// Limpiar texto previo
var cleanText = originalText.replace(/ \(.*\)$/, '');
if (optionTb < currentTb && optionTb < config.usedSpaceTb) {
$option.prop('disabled', true);
$option.text(cleanText + ' (Espaço insuficiente)');
} else if (optionTb < currentTb) {
$option.prop('disabled', true);
$option.text(cleanText + ' (Downgrade não permitido)');
} else {
$option.prop('disabled', false);
$option.text(cleanText);
}
});
// Ajustar selección si está deshabilitada
if ($storageSelect.find('option:selected').prop('disabled')) {
var $firstEnabled = $storageSelect.find('option:not(:disabled)').first();
if ($firstEnabled.length) {
$storageSelect.val($firstEnabled.val()).trigger('change');
}
}
// Mostrar alerta si es necesario
if (config.usedSpaceTb > currentTb) {
showStorageAlert(config);
}
logDebug('Storage options updated', { currentTb: currentTb, usedSpaceTb: config.usedSpaceTb });
}
function updateOfficeOptions(config) {
var currentUsers = parseInt(config.currentSuite.replace('users', '')) || 20;
var $officeSelect = $('#office_suite');
if (!$officeSelect.length) return;
$officeSelect.find('option').each(function() {
var $option = $(this);
var optionUsers = parseInt($option.val().replace('users', ''));
var originalKey = 'office_suite_' + $option.val();
var originalText = originalTextsCache[originalKey] || $option.text();
if (optionUsers < currentUsers && currentUsers > 20) {
if (originalText.indexOf('(Redução de licenças)') === -1) {
$option.text(originalText + ' - Redução de licenças');
}
} else {
$option.text(originalText);
}
$option.prop('disabled', false);
});
logDebug('Office options updated', { currentUsers: currentUsers });
}
function showStorageAlert(config) {
var alertId = 'storage_alert';
if ($('#' + alertId).length) return;
var alertHtml = '<div class="pmpro_message pmpro_error" id="' + alertId + '">' +
'<strong>Atenção:</strong> Você está usando ' + config.usedSpaceTb.toFixed(2) + ' TB de armazenamento. ' +
'Não é possível reduzir abaixo deste limite.' +
'</div>';
$('#storage_space').before(alertHtml);
logInfo('Storage alert shown', { usedSpace: config.usedSpaceTb });
}
// ====================================================================
// INICIALIZACIÓN PRINCIPAL
// ====================================================================
function initializePMProDynamic() {
logInfo('Starting PMPro Dynamic initialization', { version: PLUGIN_VERSION });
// Verificar dependencias
if (!validateDependencies()) {
logError('Dependencies validation failed');
return false;
}
// Obtener configuración
var config = getConfig();
if (!config) {
logError('Configuration not available');
return false;
}
try {
// 1. Guardar textos originales
storeOriginalTexts();
// 2. Configurar valores iniciales
var $storage = $('#storage_space');
var $office = $('#office_suite');
var $frequency = $('#payment_frequency');
if ($storage.length && !$storage.val()) {
$storage.val(config.currentStorage);
}
if ($office.length && !$office.val()) {
$office.val(config.currentSuite);
}
if ($frequency.length && !$frequency.val()) {
$frequency.val('monthly');
}
// 3. Actualizar opciones
updateStorageOptions(config);
updateOfficeOptions(config);
// 4. Calcular precio inicial
updatePriceDisplay(config);
// 5. Inicializar tooltips
initTooltips();
// 6. Configurar event listeners
$('#storage_space, #office_suite, #payment_frequency')
.off('change.pmpro')
.on('change.pmpro', function() {
logDebug('Field changed', {
field: $(this).attr('id'),
value: $(this).val()
});
// Limpiar caché
clearPriceCache();
// Actualizar precio
updatePriceDisplay(config);
});
// 7. Mostrar sección de precio
$('.pmpro_checkout-field-price-display').show();
logInfo('PMPro Dynamic initialized successfully', {
fieldsFound: $('#storage_space, #office_suite, #payment_frequency').length,
tooltipsFound: $('.pmpro-tooltip-trigger').length,
basePrice: config.basePrice
});
return true;
} catch (error) {
logError('Exception during initialization', {
message: error.message,
stack: error.stack
});
return false;
}
}
// ====================================================================
// ESTRATEGIA DE INICIALIZACIÓN MÚLTIPLE
// ====================================================================
var initializationAttempts = 0;
var maxAttempts = 5;
var initializationDelays = [100, 250, 500, 1000, 2000];
function attemptInitialization() {
if (initializationAttempts >= maxAttempts) {
logError('Maximum initialization attempts reached');
return;
}
var delay = initializationDelays[initializationAttempts] || 2000;
initializationAttempts++;
setTimeout(function() {
logDebug('Initialization attempt ' + initializationAttempts + '/' + maxAttempts);
if (initializePMProDynamic()) {
logInfo('Initialization successful');
return;
}
// Si falló, intentar de nuevo
if (initializationAttempts < maxAttempts) {
logWarn('Initialization failed, retrying in ' + initializationDelays[initializationAttempts] + 'ms');
attemptInitialization();
}
}, delay);
}
// Iniciar proceso
attemptInitialization();
// Backup: inicializar cuando la página esté cargada
$(window).on('load', function() {
if ($('.pmpro-tooltip-trigger').length === 0 || $('#total_price_display').val() === '') {
logInfo('Window load backup initialization');
setTimeout(initializePMProDynamic, 500);
}
});
// API para debugging (solo en modo debug)
if (DEBUG_MODE) {
window.PMProDynamic = {
version: PLUGIN_VERSION,
reinitialize: initializePMProDynamic,
clearCache: clearPriceCache,
getConfig: getConfig,
log: {
error: logError,
warn: logWarn,
info: logInfo,
debug: logDebug
}
};
logInfo('Debug mode enabled - API available in window.PMProDynamic');
}
});

View File

View File

View File

@ -0,0 +1,53 @@
document.addEventListener('DOMContentLoaded', () => {
initSimpleAccordion();
function initSimpleAccordion() {
const accordionHeaders = document.querySelectorAll('.simple-accordion .accordion-header');
accordionHeaders.forEach(header => {
header.addEventListener('click', () => {
toggleAccordion(header);
});
// Accesibilidad: navegación por teclado
header.addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggleAccordion(header);
}
});
});
}
function toggleAccordion(header) {
const content = header.nextElementSibling;
const accordion = header.closest('.simple-accordion');
const isActive = header.classList.contains('active');
// Cerrar otros acordeones si no es la versión de múltiples abiertos
if (!accordion.classList.contains('allow-multiple-open')) {
accordion.querySelectorAll('.accordion-header').forEach(h => {
if (h !== header && h.classList.contains('active')) {
h.classList.remove('active');
h.setAttribute('aria-expanded', 'false');
h.nextElementSibling.classList.remove('active');
}
});
}
// Toggle del acordeón actual
header.classList.toggle('active');
content.classList.toggle('active');
header.setAttribute('aria-expanded', !isActive);
// Scroll suave al acordeón abierto
if (!isActive) {
content.addEventListener('transitionend', () => {
header.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
}, { once: true });
}
}
});

View File

View File

@ -0,0 +1 @@
// Tu código JS aquí

View File

@ -0,0 +1,14 @@
{
"description": "Redirect to Homepage after logout",
"last_updated": "2025-08-15 03:24:01",
"active": true,
"hooks": {
"wp_logout": {
"type": "action",
"priority": 10,
"accepted_args": 1,
"auto_detected": true
}
},
"load_priority": 10
}

View File

@ -0,0 +1,12 @@
<?php
// Redirect to Homepage after logout
if (!defined('ABSPATH')) exit;
function auto_redirect_after_logout(){
$home_url = home_url();
wp_safe_redirect( $home_url );
exit;
}
add_action('wp_logout','auto_redirect_after_logout');

View File

@ -0,0 +1,7 @@
{
"description": "Back to Top Button",
"last_updated": "2025-08-15 00:44:46",
"active": true,
"hooks": [],
"load_priority": 10
}

View File

@ -0,0 +1,3 @@
<?php
// Back to Top Button PHP
echo '<span id="toTop" class="dashicons dashicons-arrow-up-alt2"></span>';

View File

@ -0,0 +1,7 @@
{
"description": "Responder a solicitud de plan Nextcloud TI integrada con pricing din\u00e1mico",
"last_updated": "2025-08-25 00:50:51",
"active": true,
"hooks": [],
"load_priority": 10
}

View File

@ -0,0 +1,420 @@
<?php
// @description Nuevo snippet
if (!defined('ABSPATH')) exit;
// Importar funciones de logging si están disponibles
if (!function_exists('nextcloud_log_info') && function_exists('error_log')) {
function nextcloud_log_info($message, $context = []) {
$log_message = '[Nextcloud TI] ' . $message;
if (!empty($context)) {
$log_message .= ' | Context: ' . wp_json_encode($context, JSON_UNESCAPED_UNICODE);
}
error_log($log_message);
}
}
if (!function_exists('nextcloud_log_error') && function_exists('error_log')) {
function nextcloud_log_error($message, $context = []) {
nextcloud_log_info('ERROR: ' . $message, $context);
}
}
/**
* Responder a solicitud de plan Nextcloud TI - Versión mejorada
*
* @param int $user_id ID del usuario
* @param MemberOrder $morder Objeto de orden de PMPro
*/
function plan_nextcloud_ti($user_id, $morder) {
// Validaciones iniciales
if (empty($user_id) || empty($morder)) {
nextcloud_log_error('Invalid parameters provided', [
'user_id' => $user_id,
'morder_exists' => !empty($morder)
]);
return false;
}
try {
// Generar password para la nueva cuenta Nextcloud
$password = wp_generate_password(12, false);
// Obtener información del usuario con validaciones
$user = get_userdata($user_id);
if (!$user) {
nextcloud_log_error('User not found', ['user_id' => $user_id]);
return false;
}
$email = $user->user_email;
$username = $user->user_login;
$displayname = $user->display_name ?: $username;
// Obtener nivel de membresía actual
$level = pmpro_getMembershipLevelForUser($user_id);
if (!$level) {
nextcloud_log_error('No membership level found for user', ['user_id' => $user_id]);
return false;
}
// Configurar timezone y fecha del pedido
$dt = new DateTime();
$dt->setTimezone(new DateTimeZone('America/Boa_Vista'));
// Usar timestamp del morder o timestamp actual
$order_timestamp = !empty($morder->timestamp) ? $morder->timestamp : current_time('timestamp');
$dt->setTimestamp($order_timestamp);
$fecha_pedido = $dt->format('d/m/Y H:i:s');
// Obtener configuración dinámica del usuario (del sistema de pricing dinámico)
$config_data = get_nextcloud_user_config($user_id);
// Obtener fecha del próximo pago usando PMPro
$fecha_pago_proximo = get_pmpro_next_payment_date($user_id, $level);
// Preparar datos del email
$email_data = prepare_nextcloud_email_data($user, $level, $morder, $config_data, [
'password' => $password,
'fecha_pedido' => $fecha_pedido,
'fecha_pago_proximo' => $fecha_pago_proximo
]);
// Enviar email al usuario
$user_email_sent = send_nextcloud_user_email($email_data);
// Enviar email al administrador
$admin_email_sent = send_nextcloud_admin_email($email_data);
// Log del resultado
nextcloud_log_info('Nextcloud TI plan processing completed', [
'user_id' => $user_id,
'username' => $username,
'level_name' => $level->name,
'user_email_sent' => $user_email_sent,
'admin_email_sent' => $admin_email_sent,
'config_data' => $config_data
]);
return true;
} catch (Exception $e) {
nextcloud_log_error('Exception in plan_nextcloud_ti', [
'user_id' => $user_id,
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
]);
return false;
}
}
/**
* Obtiene la configuración dinámica del usuario
*/
function get_nextcloud_user_config($user_id) {
$config_json = get_user_meta($user_id, 'nextcloud_config', true);
if (empty($config_json)) {
nextcloud_log_info('No dynamic config found for user, using defaults', ['user_id' => $user_id]);
return [
'storage_space' => '1tb',
'office_suite' => '20users',
'payment_frequency' => 'monthly',
'storage_display' => '1 Terabyte',
'office_display' => '±20 usuários (CODE - Grátis)',
'frequency_display' => 'Mensal'
];
}
$config = json_decode($config_json, true);
if (json_last_error() !== JSON_ERROR_NONE) {
nextcloud_log_error('Invalid JSON in user config', [
'user_id' => $user_id,
'json_error' => json_last_error_msg()
]);
return null;
}
// Enriquecer con información de display
$config['storage_display'] = get_storage_display_name($config['storage_space'] ?? '1tb');
$config['office_display'] = get_office_display_name($config['office_suite'] ?? '20users');
$config['frequency_display'] = get_frequency_display_name($config['payment_frequency'] ?? 'monthly');
return $config;
}
/**
* Obtiene la fecha del próximo pago usando PMPro nativo
*/
function get_pmpro_next_payment_date($user_id, $level) {
// Usar función nativa de PMPro si está disponible
if (function_exists('pmpro_next_payment')) {
$next_payment = pmpro_next_payment($user_id);
if (!empty($next_payment)) {
return date('d/m/Y', $next_payment);
}
}
// Fallback: calcular basado en el nivel y la última orden
if (class_exists('MemberOrder')) {
$last_order = new MemberOrder();
$last_order->getLastMemberOrder($user_id, 'success');
if (!empty($last_order->timestamp)) {
$last_payment_timestamp = is_numeric($last_order->timestamp)
? $last_order->timestamp
: strtotime($last_order->timestamp);
// Calcular próximo pago basado en el ciclo
$cycle_seconds = get_cycle_seconds_from_level($level);
$next_payment_timestamp = $last_payment_timestamp + $cycle_seconds;
return date('d/m/Y', $next_payment_timestamp);
}
}
// Último fallback: basado en la fecha actual y ciclo del nivel
$cycle_seconds = get_cycle_seconds_from_level($level);
$next_payment_timestamp = current_time('timestamp') + $cycle_seconds;
return date('d/m/Y', $next_payment_timestamp);
}
/**
* Calcula segundos del ciclo basado en el nivel
*/
function get_cycle_seconds_from_level($level) {
if (empty($level->cycle_number) || empty($level->cycle_period)) {
return 30 * DAY_IN_SECONDS; // Default: 30 días
}
$multipliers = [
'Day' => DAY_IN_SECONDS,
'Week' => WEEK_IN_SECONDS,
'Month' => 30 * DAY_IN_SECONDS,
'Year' => YEAR_IN_SECONDS
];
$multiplier = $multipliers[$level->cycle_period] ?? (30 * DAY_IN_SECONDS);
return $level->cycle_number * $multiplier;
}
/**
* Prepara los datos para los emails
*/
function prepare_nextcloud_email_data($user, $level, $morder, $config_data, $additional_data) {
// Determinar mensajes basados en la frecuencia
$frequency_messages = get_frequency_messages($config_data['payment_frequency'] ?? 'monthly');
return [
'user' => $user,
'level' => $level,
'morder' => $morder,
'config' => $config_data,
'password' => $additional_data['password'],
'fecha_pedido' => $additional_data['fecha_pedido'],
'fecha_pago_proximo' => $additional_data['fecha_pago_proximo'],
'monthly_message' => $frequency_messages['monthly_message'],
'date_message' => $frequency_messages['date_message']
];
}
/**
* Obtiene mensajes según la frecuencia de pago
*/
function get_frequency_messages($payment_frequency) {
$messages = [
'monthly' => [
'monthly_message' => 'mensal ',
'date_message' => 'Data do próximo pagamento: '
],
'semiannual' => [
'monthly_message' => 'semestral ',
'date_message' => 'Data da próxima cobrança semestral: '
],
'annual' => [
'monthly_message' => 'anual ',
'date_message' => 'Data da próxima cobrança anual: '
],
'biennial' => [
'monthly_message' => 'bienal ',
'date_message' => 'Data da próxima cobrança (em 2 anos): '
],
'triennial' => [
'monthly_message' => 'trienal ',
'date_message' => 'Data da próxima cobrança (em 3 anos): '
],
'quadrennial' => [
'monthly_message' => 'quadrienal ',
'date_message' => 'Data da próxima cobrança (em 4 anos): '
],
'quinquennial' => [
'monthly_message' => 'quinquenal ',
'date_message' => 'Data da próxima cobrança (em 5 anos): '
]
];
return $messages[$payment_frequency] ?? $messages['monthly'];
}
/**
* Envía email al usuario
*/
function send_nextcloud_user_email($data) {
$user = $data['user'];
$level = $data['level'];
$morder = $data['morder'];
$config = $data['config'];
// Configuración del email
$brdrv_email = "cloud@" . basename(get_site_url());
$mailto = "mailto:" . $brdrv_email;
// Título del email
$subject = "Sua instância Nextcloud será criada";
// Construir mensaje
$message = "<h1>Cloud Brasdrive</h1>";
$message .= "<p>Prezado(a) <b>" . $user->display_name . "</b> (" . $user->user_login . "),</p>";
$message .= "<p>Parabéns! Seu pagamento foi confirmado e sua instância Nextcloud será criada em breve.</p>";
// Datos de acceso
$message .= "<h3>Dados da sua conta admin do Nextcloud:</h3>";
$message .= "<p><strong>Usuário:</strong> " . $user->user_login . "<br/>";
$message .= "<strong>Senha:</strong> " . $data['password'] . "</p>";
// Detalles del plan
$message .= "<h3>Detalhes do seu plano:</h3>";
$message .= "<p><strong>Plano:</strong> " . $level->name . "<br/>";
// Agregar información de configuración dinámica si está disponible
if (!empty($config)) {
$message .= "<strong>Armazenamento:</strong> " . ($config['storage_display'] ?? 'N/A') . "<br/>";
$message .= "<strong>Suite Office:</strong> " . ($config['office_display'] ?? 'N/A') . "<br/>";
$message .= "<strong>Frequência:</strong> " . ($config['frequency_display'] ?? 'N/A') . "<br/>";
}
$message .= "<strong>Data do pedido:</strong> " . $data['fecha_pedido'] . "<br/>";
$message .= "<strong>Valor " . $data['monthly_message'] . ":</strong> R$ " . number_format($morder->total, 2, ',', '.') . "<br/>";
$message .= $data['date_message'] . $data['fecha_pago_proximo'] . "</p>";
// Recomendações de segurança
$message .= "<div style='background-color: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; margin: 20px 0; border-radius: 5px;'>";
$message .= "<p><strong>⚠️ Importante - Segurança:</strong><br/>";
$message .= "Por segurança, recomendamos:</p>";
$message .= "<ul>";
$message .= "<li>Manter guardada a senha da instância Nextcloud em um local seguro</li>";
$message .= "<li>Excluir este e-mail após salvar as informações</li>";
$message .= "<li>Alterar sua senha nas Configurações pessoais de usuário da sua instância Nextcloud</li>";
$message .= "</ul></div>";
// Informações de contato
$message .= "<p>Se você tiver alguma dúvida, entre em contato conosco no e-mail: <a href='" . $mailto . "'>" . $brdrv_email . "</a>.</p>";
$message .= "<p>Atenciosamente,<br/><strong>Equipe Brasdrive</strong></p>";
$headers = array('Content-Type: text/html; charset=UTF-8');
// Enviar email
$result = wp_mail($user->user_email, $subject, $message, $headers);
if (!$result) {
nextcloud_log_error('Failed to send user email', [
'user_id' => $user->ID,
'email' => $user->user_email
]);
}
return $result;
}
/**
* Envía email al administrador
*/
function send_nextcloud_admin_email($data) {
$user = $data['user'];
$level = $data['level'];
$config = $data['config'];
$to = get_option('admin_email');
$subject = "Nova instância Nextcloud TI - " . $level->name;
$admin_message = "<h2>Nova instância Nextcloud TI contratada</h2>";
$admin_message .= "<p><strong>Plano:</strong> " . $level->name . "<br/>";
$admin_message .= "<strong>Nome:</strong> " . $user->display_name . "<br/>";
$admin_message .= "<strong>Usuário:</strong> " . $user->user_login . "<br/>";
$admin_message .= "<strong>Email:</strong> " . $user->user_email . "<br/>";
$admin_message .= "<strong>Senha gerada:</strong> " . $data['password'] . "</p>";
// Agregar configuración dinámica
if (!empty($config)) {
$admin_message .= "<h3>Configuração do plano:</h3>";
$admin_message .= "<p><strong>Armazenamento:</strong> " . ($config['storage_display'] ?? 'N/A') . "<br/>";
$admin_message .= "<strong>Suite Office:</strong> " . ($config['office_display'] ?? 'N/A') . "<br/>";
$admin_message .= "<strong>Frequência:</strong> " . ($config['frequency_display'] ?? 'N/A') . "</p>";
}
$admin_message .= "<p><strong>Data do pedido:</strong> " . $data['fecha_pedido'] . "<br/>";
$admin_message .= "<strong>Próximo pagamento:</strong> " . $data['fecha_pago_proximo'] . "</p>";
$headers = array('Content-Type: text/html; charset=UTF-8');
$result = wp_mail($to, $subject, $admin_message, $headers);
if (!$result) {
nextcloud_log_error('Failed to send admin email', [
'admin_email' => $to,
'user_id' => $user->ID
]);
}
return $result;
}
// Funciones auxiliares para nombres de display
function get_storage_display_name($storage_space) {
$storage_options = [
'1tb' => '1 Terabyte', '2tb' => '2 Terabytes', '3tb' => '3 Terabytes',
'4tb' => '4 Terabytes', '5tb' => '5 Terabytes', '6tb' => '6 Terabytes',
'7tb' => '7 Terabytes', '8tb' => '8 Terabytes', '9tb' => '9 Terabytes',
'10tb' => '10 Terabytes', '15tb' => '15 Terabytes', '20tb' => '20 Terabytes',
'30tb' => '30 Terabytes', '40tb' => '40 Terabytes', '50tb' => '50 Terabytes',
'60tb' => '60 Terabytes', '70tb' => '70 Terabytes', '80tb' => '80 Terabytes',
'90tb' => '90 Terabytes', '100tb' => '100 Terabytes', '200tb' => '200 Terabytes',
'300tb' => '300 Terabytes', '400tb' => '400 Terabytes', '500tb' => '500 Terabytes'
];
return $storage_options[$storage_space] ?? $storage_space;
}
function get_office_display_name($office_suite) {
$office_options = [
'20users' => '±20 usuários',
'30users' => '30 usuários',
'50users' => '50 usuários',
'80users' => '80 usuários',
'100users' => '100 usuários',
'150users' => '150 usuários',
'200users' => '200 usuários',
'300users' => '300 usuários',
'400users' => '400 usuários',
'500users' => '500 usuários'
];
return $office_options[$office_suite] ?? $office_suite;
}
function get_frequency_display_name($payment_frequency) {
$frequency_options = [
'monthly' => 'Mensal',
'semiannual' => 'Semestral',
'annual' => 'Anual',
'biennial' => 'Bienal',
'triennial' => 'Trienal',
'quadrennial' => 'Quadrienal',
'quinquennial' => 'Quinquenal'
];
return $frequency_options[$payment_frequency] ?? $payment_frequency;
}

View File

@ -0,0 +1,7 @@
{
"description": "Current year shortcode",
"last_updated": "2025-08-15 02:55:04",
"active": true,
"hooks": [],
"load_priority": 10
}

View File

@ -0,0 +1,9 @@
<?php
// Current year shortcode
if (!defined('ABSPATH')) exit;
function current_year() {
return date('Y');
}
add_shortcode( 'year', 'current_year' );

View File

@ -0,0 +1,14 @@
{
"description": "Remove WP logo from login page",
"last_updated": "2025-08-15 03:21:27",
"active": true,
"hooks": {
"login_head": {
"type": "action",
"priority": 10,
"accepted_args": 1,
"auto_detected": true
}
},
"load_priority": 10
}

View File

@ -0,0 +1,9 @@
<?php
// Remove WP logo from login page
if (!defined('ABSPATH')) exit;
function custom_login_logo() {
echo '<style type ="text/css">.login h1 a { visibility:hidden!important; }</style>';
}
add_action('login_head', 'custom_login_logo');

View File

@ -0,0 +1,20 @@
{
"description": "Monitorear el espacio en Nextcloud y enviar reporte diario",
"last_updated": "2025-08-12 02:38:43",
"active": false,
"hooks": {
"wp": {
"type": "action",
"priority": 10,
"accepted_args": 1,
"auto_detected": true
},
"daily_nextcloud_storage_check": {
"type": "action",
"priority": 10,
"accepted_args": 1,
"auto_detected": true
}
},
"load_priority": 10
}

View File

@ -0,0 +1,67 @@
<?php
// Monitorear el espacio en Nextcloud y enviar reporte diario
if (!defined('ABSPATH')) exit;
function get_nextcloud_storage_via_reports_api() {
$nextcloud_api_admin = getenv('NEXTCLOUD_API_ADMIN');
$nextcloud_api_pass = getenv('NEXTCLOUD_API_PASS');
$current_date = date('Y-m-d', time());
$site_url = get_option('siteurl');
$to_admin = get_option('admin_email');
$headers = array('Content-Type: text/html; charset=UTF-8');
$nextcloud_url = 'https://cloud.' . basename($site_url);
$max_storage = 1000; // 1 TB en GB
$endpoint = $nextcloud_url . '/ocs/v2.php/apps/serverinfo/api/v1/info?format=json';
$args = array(
'headers' => array(
'OCS-APIRequest' => 'true',
'Authorization' => 'Basic ' . base64_encode($nextcloud_api_admin . ':' . $nextcloud_api_pass),
),
);
$response = wp_remote_get($endpoint, $args);
if (!is_wp_error($response)) {
$body = json_decode(wp_remote_retrieve_body($response), true);
if (isset($body['ocs']['data']['nextcloud']['storage']['users'])) {
$total_used = $body['ocs']['data']['nextcloud']['storage']['used'];
$total_free = $body['ocs']['data']['nextcloud']['storage']['free'];
$total_space = $total_used + $total_free;
return array(
'used' => round($total_used / (1024 * 1024 * 1024), 2), // Convertir a GB
'total' => round($total_space / (1024 * 1024 * 1024), 2)
);
}
}
// Preparar y enviar el email
$to = get_option('admin_email');
$subject = 'Reporte de Almacenamiento Cloud Brasdrive';
$message = sprintf(
$current_date,
'Uso de almacenamiento en Cloud Brasdrive: %.2f GB de %d GB (%.1f%%) utilizados.',
$total_used,
$max_storage,
($total_used / $max_storage) * 100
);
wp_mail($to, $subject, $message);
return false;
}
// Programar el evento diario
function schedule_nextcloud_storage_check() {
if (!wp_next_scheduled('daily_nextcloud_storage_check')) {
wp_schedule_event(strtotime('01:00:00'), 'daily', 'daily_nextcloud_storage_check');
}
}
add_action('wp', 'schedule_nextcloud_storage_check');
// Conectar la acción programada con nuestra función
add_action('daily_nextcloud_storage_check', 'get_nextcloud_storage_via_reports_api');

View File

@ -0,0 +1,50 @@
{
"description": "PMPro Dynamic Pricing para el Plan Nextcloud Banda",
"last_updated": "2025-10-04 03:09:49",
"active": true,
"hooks": {
"admin_notices": {
"type": "action",
"priority": 10,
"accepted_args": 1,
"auto_detected": true
},
"plugins_loaded": {
"type": "action",
"priority": 25,
"accepted_args": 1,
"auto_detected": true
},
"init": {
"type": "action",
"priority": 10,
"accepted_args": 1,
"auto_detected": true
},
"wp_loaded": {
"type": "action",
"priority": 5,
"accepted_args": 1,
"auto_detected": true
},
"pmpro_checkout_level": {
"type": "filter",
"priority": 1,
"accepted_args": 1,
"auto_detected": true
},
"pmpro_after_checkout": {
"type": "action",
"priority": 10,
"accepted_args": 2,
"auto_detected": true
},
"pmpro_account_bullets_bottom": {
"type": "action",
"priority": 10,
"accepted_args": 1,
"auto_detected": true
}
},
"load_priority": 10
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,98 @@
{
"description": "Agregar campos din\u00e1micos usando PMPro Register Helper",
"last_updated": "2025-08-24 17:23:24",
"active": true,
"hooks": {
"admin_notices": {
"type": "action",
"priority": 10,
"accepted_args": 1,
"auto_detected": true
},
"plugins_loaded": {
"type": "action",
"priority": 25,
"accepted_args": 1,
"auto_detected": true
},
"init": {
"type": "action",
"priority": 20,
"accepted_args": 1,
"auto_detected": true
},
"wp_loaded": {
"type": "action",
"priority": 5,
"accepted_args": 1,
"auto_detected": true
},
"wp_enqueue_scripts": {
"type": "action",
"priority": 30,
"accepted_args": 1,
"auto_detected": true
},
"pmpro_checkout_level": {
"type": "filter",
"priority": 1,
"accepted_args": 1,
"auto_detected": true
},
"pmpro_after_checkout_level": {
"type": "filter",
"priority": 1,
"accepted_args": 1,
"auto_detected": true
},
"pmpro_checkout_level_cost_text": {
"type": "filter",
"priority": 10,
"accepted_args": 2,
"auto_detected": true
},
"pmpro_checkout_before_processing": {
"type": "action",
"priority": 1,
"accepted_args": 1,
"auto_detected": true
},
"pmpro_checkout_order": {
"type": "filter",
"priority": 1,
"accepted_args": 1,
"auto_detected": true
},
"pmpro_checkout_before_payment": {
"type": "action",
"priority": 1,
"accepted_args": 1,
"auto_detected": true
},
"pmpro_after_checkout": {
"type": "action",
"priority": 10,
"accepted_args": 2,
"auto_detected": true
},
"pmpro_account_bullets_bottom": {
"type": "action",
"priority": 10,
"accepted_args": 1,
"auto_detected": true
},
"wp_footer": {
"type": "action",
"priority": 10,
"accepted_args": 1,
"auto_detected": true
},
"admin_bar_menu": {
"type": "action",
"priority": 10,
"accepted_args": 1,
"auto_detected": true
}
},
"load_priority": 10
}

View File

@ -0,0 +1,995 @@
<?php
/**
* PMPro Dynamic Pricing for Nextcloud Storage Plans - SNIPPET OPTIMIZADO v2.1
*
* Versión optimizada que mantiene la estructura de snippet con:
* - Logging optimizado y configuración centralizada
* - Sistema de caché mejorado
* - Validaciones robustas y manejo de errores
* - Optimizaciones de rendimiento
* - Compatibilidad mejorada
*
* @version 2.1.0
*/
if (!defined('ABSPATH')) {
exit('Acceso directo no permitido');
}
// ====================================================================
// CONFIGURACIÓN GLOBAL Y CONSTANTES
// ====================================================================
define('NEXTCLOUD_PLUGIN_VERSION', '2.1.0');
define('NEXTCLOUD_CACHE_GROUP', 'nextcloud_dynamic');
define('NEXTCLOUD_CACHE_EXPIRY', HOUR_IN_SECONDS);
/**
* Configuración centralizada - optimizada
*/
function nextcloud_get_config($key = null) {
static $config = null;
if ($config === null) {
$config = [
'allowed_levels' => [10, 11, 12, 13, 14],
'price_per_tb' => 120.00,
'office_user_price' => 25.00,
'frequency_multipliers' => [
'monthly' => 1.0,
'semiannual' => 5.7,
'annual' => 10.8,
'biennial' => 20.4,
'triennial' => 28.8,
'quadrennial' => 36.0,
'quinquennial' => 42.0
],
'storage_options' => [
'1tb' => '1 Terabyte', '2tb' => '2 Terabytes', '3tb' => '3 Terabytes',
'4tb' => '4 Terabytes', '5tb' => '5 Terabytes', '6tb' => '6 Terabytes',
'7tb' => '7 Terabytes', '8tb' => '8 Terabytes', '9tb' => '9 Terabytes',
'10tb' => '10 Terabytes', '15tb' => '15 Terabytes', '20tb' => '20 Terabytes',
'30tb' => '30 Terabytes', '40tb' => '40 Terabytes', '50tb' => '50 Terabytes',
'60tb' => '60 Terabytes', '70tb' => '70 Terabytes', '80tb' => '80 Terabytes',
'90tb' => '90 Terabytes', '100tb' => '100 Terabytes', '200tb' => '200 Terabytes',
'300tb' => '300 Terabytes', '400tb' => '400 Terabytes', '500tb' => '500 Terabytes'
],
'office_options' => [
'20users' => '±20 usuários (CODE - Grátis)',
'30users' => '30 usuários (Business)',
'50users' => '50 usuários (Business)',
'80users' => '80 usuários (Business)',
'100users' => '100 usuários (Enterprise, -15%)',
'150users' => '150 usuários (Enterprise, -15%)',
'200users' => '200 usuários (Enterprise, -15%)',
'300users' => '300 usuários (Enterprise, -15%)',
'400users' => '400 usuários (Enterprise, -15%)',
'500users' => '500 usuários (Enterprise, -15%)'
]
];
}
return $key ? ($config[$key] ?? null) : $config;
}
// ====================================================================
// SISTEMA DE LOGGING OPTIMIZADO
// ====================================================================
/**
* Logging centralizado con niveles
*/
function nextcloud_log($level, $message, $context = []) {
static $log_level = null;
if ($log_level === null) {
if (defined('WP_DEBUG') && WP_DEBUG) {
$log_level = 4; // DEBUG
} elseif (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) {
$log_level = 3; // INFO
} else {
$log_level = 1; // ERROR only
}
}
$levels = [1 => 'ERROR', 2 => 'WARNING', 3 => 'INFO', 4 => 'DEBUG'];
if ($level > $log_level) return;
$log_message = sprintf(
'[PMPro Dynamic %s] %s',
$levels[$level],
$message
);
if (!empty($context)) {
$log_message .= ' | Context: ' . wp_json_encode($context, JSON_UNESCAPED_UNICODE);
}
error_log($log_message);
}
// Funciones de logging simplificadas
function nextcloud_log_error($message, $context = []) {
nextcloud_log(1, $message, $context);
}
function nextcloud_log_warning($message, $context = []) {
nextcloud_log(2, $message, $context);
}
function nextcloud_log_info($message, $context = []) {
nextcloud_log(3, $message, $context);
}
function nextcloud_log_debug($message, $context = []) {
nextcloud_log(4, $message, $context);
}
// ====================================================================
// SISTEMA DE CACHÉ OPTIMIZADO
// ====================================================================
/**
* Obtener datos del caché
*/
function nextcloud_cache_get($key, $default = false) {
$cached = wp_cache_get($key, NEXTCLOUD_CACHE_GROUP);
if ($cached !== false) {
nextcloud_log_debug("Cache hit for key: {$key}");
return $cached;
}
nextcloud_log_debug("Cache miss for key: {$key}");
return $default;
}
/**
* Guardar datos en caché
*/
function nextcloud_cache_set($key, $data, $expiry = NEXTCLOUD_CACHE_EXPIRY) {
$result = wp_cache_set($key, $data, NEXTCLOUD_CACHE_GROUP, $expiry);
nextcloud_log_debug("Cache set for key: {$key}", ['success' => $result]);
return $result;
}
/**
* Eliminar datos del caché
*/
function nextcloud_cache_delete($key) {
$result = wp_cache_delete($key, NEXTCLOUD_CACHE_GROUP);
nextcloud_log_debug("Cache deleted for key: {$key}", ['success' => $result]);
return $result;
}
/**
* Invalidar caché de usuario
*/
function nextcloud_invalidate_user_cache($user_id) {
$keys = [
"nextcloud_config_{$user_id}",
"pmpro_membership_{$user_id}",
"nextcloud_used_space_{$user_id}",
"last_payment_date_{$user_id}"
];
foreach ($keys as $key) {
nextcloud_cache_delete($key);
}
nextcloud_log_info("User cache invalidated", ['user_id' => $user_id]);
}
// ====================================================================
// VERIFICACIÓN DE DEPENDENCIAS OPTIMIZADA
// ====================================================================
/**
* Verifica que los plugins requeridos estén activos
*/
function nextcloud_check_dependencies() {
static $dependencies_checked = false;
static $dependencies_ok = false;
if ($dependencies_checked) {
return $dependencies_ok;
}
$missing_plugins = [];
// Verificaciones críticas
if (!function_exists('pmprorh_add_registration_field')) {
$missing_plugins[] = 'PMPro Register Helper';
nextcloud_log_error('PMPro Register Helper functions not found');
}
if (!function_exists('pmpro_getOption')) {
$missing_plugins[] = 'Paid Memberships Pro';
nextcloud_log_error('PMPro core functions not found');
}
if (!class_exists('PMProRH_Field')) {
$missing_plugins[] = 'PMProRH_Field class';
nextcloud_log_error('PMProRH_Field class not available');
}
// Verificación opcional
if (!class_exists('MemberOrder')) {
nextcloud_log_warning('MemberOrder class not available - some features may be limited');
}
// Admin notice
if (!empty($missing_plugins) && is_admin() && current_user_can('manage_options')) {
add_action('admin_notices', function() use ($missing_plugins) {
$plugins_list = implode(', ', $missing_plugins);
printf(
'<div class="notice notice-error"><p><strong>PMPro Dynamic Pricing:</strong> Los siguientes plugins son requeridos: %s</p></div>',
esc_html($plugins_list)
);
});
}
$dependencies_ok = empty($missing_plugins);
$dependencies_checked = true;
nextcloud_log_info('Dependencies check completed', [
'success' => $dependencies_ok,
'missing_count' => count($missing_plugins)
]);
return $dependencies_ok;
}
// ====================================================================
// DETECCIÓN DE NIVEL ACTUAL OPTIMIZADA
// ====================================================================
/**
* Detecta el Level ID actual con múltiples estrategias
*/
function nextcloud_get_current_level_id() {
static $cached_level_id = null;
if ($cached_level_id !== null) {
return $cached_level_id;
}
// Estrategias de detección en orden de prioridad
$detectors = [
'global_checkout_level' => function() {
global $pmpro_checkout_level;
return isset($pmpro_checkout_level->id) ? (int)$pmpro_checkout_level->id : 0;
},
'get_level' => function() {
return !empty($_GET['level']) ? (int)sanitize_text_field($_GET['level']) : 0;
},
'get_pmpro_level' => function() {
return !empty($_GET['pmpro_level']) ? (int)sanitize_text_field($_GET['pmpro_level']) : 0;
},
'post_level' => function() {
return !empty($_POST['level']) ? (int)sanitize_text_field($_POST['level']) : 0;
},
'post_pmpro_level' => function() {
return !empty($_POST['pmpro_level']) ? (int)sanitize_text_field($_POST['pmpro_level']) : 0;
},
'global_level' => function() {
global $pmpro_level;
return isset($pmpro_level->id) ? (int)$pmpro_level->id : 0;
},
'session_level' => function() {
return !empty($_SESSION['pmpro_level']) ? (int)$_SESSION['pmpro_level'] : 0;
}
];
foreach ($detectors as $source => $detector) {
$level_id = $detector();
if ($level_id > 0) {
nextcloud_log_debug("Level ID detected from {$source}: {$level_id}");
$cached_level_id = $level_id;
return $level_id;
}
}
// Fallback: extraer de URL
if (function_exists('pmpro_getOption')) {
$checkout_page_slug = pmpro_getOption('checkout_page_slug');
if (!empty($checkout_page_slug) && is_page($checkout_page_slug)) {
$request_uri = $_SERVER['REQUEST_URI'] ?? '';
if (preg_match('/(?:^|[?&])(level|pmpro_level)=(\d+)/', $request_uri, $matches)) {
$level_id = (int)$matches[2];
nextcloud_log_debug("Level ID extracted from URL: {$level_id}");
$cached_level_id = $level_id;
return $level_id;
}
}
}
$cached_level_id = 0;
nextcloud_log_warning('Could not detect Level ID, using default 0');
return 0;
}
// ====================================================================
// CAMPOS DINÁMICOS OPTIMIZADOS
// ====================================================================
/**
* Añade campos dinámicos con validación robusta
*/
function nextcloud_add_dynamic_fields() {
static $fields_added = false;
static $initialization_attempted = false;
if ($initialization_attempted && !$fields_added) {
return false;
}
$initialization_attempted = true;
if ($fields_added) {
nextcloud_log_debug('Dynamic fields already added, skipping');
return true;
}
nextcloud_log_info('Attempting to add dynamic fields');
if (!nextcloud_check_dependencies()) {
nextcloud_log_error('Dependencies missing, cannot add fields');
return false;
}
$current_level_id = nextcloud_get_current_level_id();
$allowed_levels = nextcloud_get_config('allowed_levels');
if (!in_array($current_level_id, $allowed_levels, true)) {
nextcloud_log_info("Level {$current_level_id} not in allowed levels, skipping fields");
return false;
}
try {
$config = nextcloud_get_config();
$fields = [];
// Campo de almacenamiento
$fields[] = new PMProRH_Field(
'storage_space',
'select',
[
'label' => 'Espaço de armazenamento',
'options' => $config['storage_options'],
'profile' => true,
'required' => false,
'memberslistcsv' => true,
'addmember' => true,
'location' => 'after_level'
]
);
// Campo de suite ofimática
$fields[] = new PMProRH_Field(
'office_suite',
'select',
[
'label' => 'Nextcloud Office <span class="pmpro-tooltip-trigger dashicons dashicons-editor-help" data-tooltip-id="office-suite-tooltip"></span>',
'options' => $config['office_options'],
'profile' => true,
'required' => false,
'memberslistcsv' => true,
'addmember' => true,
'location' => 'after_level',
'divclass' => 'pmpro_checkout-field-office-suite bordered-field'
]
);
// Campo de frecuencia
$frequency_options = [
'monthly' => 'Mensal',
'semiannual' => 'Semestral (-5%)',
'annual' => 'Anual (-10%)',
'biennial' => 'Bienal (-15%)',
'triennial' => 'Trienal (-20%)',
'quadrennial' => 'Quadrienal (-25%)',
'quinquennial' => 'Quinquenal (-30%)'
];
$fields[] = new PMProRH_Field(
'payment_frequency',
'select',
[
'label' => 'Frequência de pagamento',
'options' => $frequency_options,
'profile' => true,
'required' => false,
'memberslistcsv' => true,
'addmember' => true,
'location' => 'after_level'
]
);
// Campo de precio total
$fields[] = new PMProRH_Field(
'total_price_display',
'text',
[
'label' => 'Preço total',
'profile' => false,
'required' => false,
'memberslistcsv' => false,
'addmember' => false,
'readonly' => true,
'location' => 'after_level',
'showrequired' => false,
'divclass' => 'pmpro_checkout-field-price-display',
'default' => 'R$ 0,00'
]
);
// Añadir campos
$fields_added_count = 0;
foreach($fields as $field) {
pmprorh_add_registration_field('Configuração do plano', $field);
$fields_added_count++;
}
$fields_added = true;
nextcloud_log_info("Dynamic fields added successfully", [
'level_id' => $current_level_id,
'fields_count' => $fields_added_count
]);
return true;
} catch (Exception $e) {
nextcloud_log_error('Exception adding dynamic fields', [
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
]);
if (defined('WP_DEBUG') && WP_DEBUG) {
wp_die('Error en el sistema de membresías: ' . esc_html($e->getMessage()));
}
return false;
}
}
// ====================================================================
// CÁLCULOS DE PRECIO OPTIMIZADOS
// ====================================================================
/**
* Calcula el precio total con caché y validaciones
*/
function nextcloud_calculate_pricing($storage_space, $office_suite, $payment_frequency, $base_price) {
// Validar parámetros
if (empty($storage_space) || empty($office_suite) || empty($payment_frequency)) {
nextcloud_log_warning('Missing parameters for price calculation');
return $base_price;
}
// Verificar caché
$cache_key = "pricing_{$storage_space}_{$office_suite}_{$payment_frequency}_{$base_price}";
$cached_price = nextcloud_cache_get($cache_key);
if ($cached_price !== false) {
return $cached_price;
}
$config = nextcloud_get_config();
$price_per_tb = $config['price_per_tb'];
$office_user_price = $config['office_user_price'];
// Calcular precio de almacenamiento
$storage_tb = (int)str_replace('tb', '', $storage_space);
$storage_price = $base_price + ($price_per_tb * max(0, $storage_tb - 1));
// Calcular precio de suite ofimática
$office_users = (int)str_replace('users', '', $office_suite);
$office_user_price = ($office_users < 100) ? $office_user_price : ($office_user_price - 3.75);
$office_price = ($office_users <= 20) ? 0 : ($office_user_price * $office_users);
// Aplicar multiplicador de frecuencia
$multipliers = $config['frequency_multipliers'];
$frequency_multiplier = $multipliers[$payment_frequency] ?? 1.0;
// Calcular precio total
$total_price = ceil(($storage_price + $office_price) * $frequency_multiplier);
// Validar resultado
if ($total_price < $base_price || $total_price > ($base_price * 100)) {
nextcloud_log_warning('Calculated price seems unreasonable', [
'total_price' => $total_price,
'base_price' => $base_price
]);
}
// Guardar en caché
nextcloud_cache_set($cache_key, $total_price, 300); // 5 minutos
nextcloud_log_debug('Price calculated', [
'storage_space' => $storage_space,
'office_suite' => $office_suite,
'payment_frequency' => $payment_frequency,
'total_price' => $total_price
]);
return $total_price;
}
/**
* Configura la periodicidad del nivel
*/
function nextcloud_configure_billing_period($level, $payment_frequency, $total_price) {
if (empty($level) || !is_object($level)) {
nextcloud_log_error('Invalid level object provided');
return $level;
}
$billing_cycles = [
'monthly' => ['number' => 1, 'period' => 'Month'],
'semiannual' => ['number' => 6, 'period' => 'Month'],
'annual' => ['number' => 12, 'period' => 'Month'],
'biennial' => ['number' => 24, 'period' => 'Month'],
'triennial' => ['number' => 36, 'period' => 'Month'],
'quadrennial' => ['number' => 48, 'period' => 'Month'],
'quinquennial' => ['number' => 60, 'period' => 'Month']
];
$cycle_config = $billing_cycles[$payment_frequency] ?? $billing_cycles['monthly'];
$level->cycle_number = $cycle_config['number'];
$level->cycle_period = $cycle_config['period'];
$level->billing_amount = $total_price;
$level->initial_payment = $total_price;
$level->trial_amount = 0;
$level->trial_limit = 0;
$level->recurring = true;
// Preservar configuración de expiración
if (!isset($level->expiration_number) || empty($level->expiration_number)) {
$level->expiration_number = 0;
$level->expiration_period = '';
nextcloud_log_debug('Level configured as unlimited');
}
nextcloud_log_info('Billing period configured', [
'payment_frequency' => $payment_frequency,
'cycle_number' => $level->cycle_number,
'billing_amount' => $level->billing_amount
]);
return $level;
}
// ====================================================================
// FUNCIONES AUXILIARES OPTIMIZADAS
// ====================================================================
/**
* Obtiene el espacio usado en Nextcloud (placeholder)
*/
function get_nextcloud_used_space_tb($user_id) {
// TODO: Implementar conexión real a API de Nextcloud
$cache_key = "used_space_{$user_id}";
$cached = nextcloud_cache_get($cache_key);
if ($cached !== false) {
return $cached;
}
// Placeholder - implementar API real
$used_space_mb = 1200;
$used_space_tb = round($used_space_mb / 1024 / 1024, 2);
nextcloud_cache_set($cache_key, $used_space_tb, 300);
return $used_space_tb;
}
/**
* Obtiene el espacio usado desde Nextcloud (placeholder)
*/
function get_nextcloud_used_storage($user_id) {
// TODO: Implementar API call real a Nextcloud
return 0;
}
/**
* Calcula días restantes en el ciclo actual
*/
function get_remaining_days_in_cycle($user_id, $frequency) {
if (!class_exists('MemberOrder')) {
nextcloud_log_warning('MemberOrder class not available');
return 0;
}
$last_order = new MemberOrder();
$last_order->getLastMemberOrder($user_id, 'success');
if (empty($last_order->timestamp)) {
return 0;
}
$last_payment_date = is_numeric($last_order->timestamp)
? $last_order->timestamp
: strtotime($last_order->timestamp);
$cycle_days = get_cycle_days($frequency);
$days_used = floor((current_time('timestamp') - $last_payment_date) / DAY_IN_SECONDS);
return max(0, $cycle_days - $days_used);
}
/**
* Obtiene la duración en días del ciclo
*/
function get_cycle_days($frequency) {
$cycles = [
'monthly' => 30,
'semiannual' => 180,
'annual' => 365,
'biennial' => 730,
'triennial' => 1095,
'quadrennial' => 1460,
'quinquennial' => 1825
];
return $cycles[$frequency] ?? 30;
}
// ====================================================================
// SISTEMA DE PRORRATEO OPTIMIZADO
// ====================================================================
/**
* Valida si un cambio de plan es permitido
*/
function is_change_allowed($current_config, $new_config) {
$cur_storage = (int)str_replace('tb', '', $current_config['storage_space']);
$new_storage = (int)str_replace('tb', '', $new_config['storage_space']);
// Verificar downgrade de almacenamiento
if ($new_storage < $cur_storage) {
$used = get_nextcloud_used_space_tb(get_current_user_id());
if ($used > $new_storage) {
return [false, "Não é possível reduzir para {$new_storage} TB pois você usa {$used} TB"];
}
}
return [true, ""];
}
/**
* Valida y aplica prorrateo para cambios
*/
function nextcloud_validate_and_prorate_changes($level) {
$user_id = get_current_user_id();
// Cargar configuración actual
$cache_key = "nextcloud_config_{$user_id}";
$current_config_json = nextcloud_cache_get($cache_key);
if ($current_config_json === false) {
$current_config_json = get_user_meta($user_id, 'nextcloud_config', true);
nextcloud_cache_set($cache_key, $current_config_json);
}
$current_config = $current_config_json ? json_decode($current_config_json, true) : null;
// Si no hay configuración previa → nuevo registro
if (!$current_config) {
nextcloud_log_debug("New registration detected for user {$user_id}");
return $level;
}
// Obtener nuevas selecciones
$new_storage = sanitize_text_field($_POST['storage_space'] ?? $current_config['storage_space']);
$new_suite = sanitize_text_field($_POST['office_suite'] ?? ($current_config['office_suite'] ?? '20users'));
$new_frequency = sanitize_text_field($_POST['payment_frequency'] ?? $current_config['payment_frequency']);
// Detectar cambios
$has_changes = (
$new_storage !== $current_config['storage_space'] ||
$new_suite !== ($current_config['office_suite'] ?? '20users') ||
$new_frequency !== $current_config['payment_frequency']
);
if (!$has_changes) {
nextcloud_log_debug("No changes detected for user {$user_id}");
return $level;
}
// Preparar nueva configuración
$new_config = [
'storage_space' => $new_storage,
'office_suite' => $new_suite,
'payment_frequency' => $new_frequency,
'level_id' => $level->id
];
// Validar cambio
list($allowed, $error_message) = is_change_allowed($current_config, $new_config);
if (!$allowed) {
nextcloud_log_error("Change rejected for user {$user_id}: {$error_message}");
wp_die(__($error_message, 'pmpro'));
}
// Aplicar prorrateo si es upgrade de almacenamiento
$current_tb = (int)str_replace('tb', '', $current_config['storage_space']);
$new_tb = (int)str_replace('tb', '', $new_storage);
if ($new_tb > $current_tb) {
$level = apply_storage_upgrade_prorate($level, $user_id, $current_tb, $new_tb, $current_config['payment_frequency']);
nextcloud_log_info("Storage upgrade applied with prorate for user {$user_id}");
}
return $level;
}
/**
* Aplica prorrateo para upgrade de almacenamiento
*/
function apply_storage_upgrade_prorate($level, $user_id, $current_tb, $new_tb, $current_frequency) {
$price_per_tb = nextcloud_get_config('price_per_tb');
$full_price_diff = ($new_tb - $current_tb) * $price_per_tb;
$days_remaining = get_remaining_days_in_cycle($user_id, $current_frequency);
if ($days_remaining > 0) {
$total_days = get_cycle_days($current_frequency);
$prorated_amount = ($full_price_diff / $total_days) * $days_remaining;
$level->initial_payment += round($prorated_amount, 2);
nextcloud_log_info("Prorate applied", [
'user_id' => $user_id,
'upgrade_tb' => $new_tb - $current_tb,
'days_remaining' => $days_remaining,
'prorated_amount' => $prorated_amount
]);
}
return $level;
}
// ====================================================================
// HOOKS Y FILTROS PRINCIPALES
// ====================================================================
/**
* Hook principal de modificación de precio
*/
function nextcloud_modify_level_pricing($level) {
// Prevenir procesamiento múltiple
if (!empty($level->_nextcloud_applied)) {
nextcloud_log_debug('Level pricing already applied');
return $level;
}
$allowed_levels = nextcloud_get_config('allowed_levels');
if (!in_array((int)$level->id, $allowed_levels, true)) {
return $level;
}
$required_fields = ['storage_space', 'office_suite', 'payment_frequency'];
foreach ($required_fields as $field) {
if (!isset($_POST[$field]) || empty($_POST[$field])) {
nextcloud_log_debug("Required field {$field} missing");
return $level;
}
}
try {
// Aplicar validaciones y prorrateo
$level = nextcloud_validate_and_prorate_changes($level);
// Sanitizar entrada
$storage_space = sanitize_text_field($_POST['storage_space']);
$office_suite = sanitize_text_field($_POST['office_suite']);
$payment_frequency = sanitize_text_field($_POST['payment_frequency']);
// Obtener precio base original
$original_level = pmpro_getLevel($level->id);
$base_price = $original_level ? (float)$original_level->initial_payment : (float)$level->initial_payment;
// Calcular precio total
$total_price = nextcloud_calculate_pricing($storage_space, $office_suite, $payment_frequency, $base_price);
// Aplicar configuración
$level->initial_payment = $total_price;
$level = nextcloud_configure_billing_period($level, $payment_frequency, $total_price);
$level->_nextcloud_applied = true;
nextcloud_log_info('Level pricing modified successfully', [
'level_id' => $level->id,
'final_price' => $total_price,
'storage_space' => $storage_space,
'office_suite' => $office_suite,
'payment_frequency' => $payment_frequency
]);
} catch (Exception $e) {
nextcloud_log_error('Exception in nextcloud_modify_level_pricing', [
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
]);
}
return $level;
}
/**
* Guardado optimizado de configuración
*/
function nextcloud_save_configuration_and_provision($user_id, $morder) {
if (!$user_id || !$morder) {
nextcloud_log_error('Invalid parameters for save_configuration');
return;
}
$required_fields = ['storage_space', 'office_suite', 'payment_frequency'];
$config_data = [];
foreach ($required_fields as $field) {
if (!isset($_REQUEST[$field]) || empty($_REQUEST[$field])) {
nextcloud_log_warning("Missing {$field} in configuration save");
return;
}
$config_data[$field] = sanitize_text_field($_REQUEST[$field]);
}
$config = array_merge($config_data, [
'created_at' => current_time('mysql'),
'updated_at' => current_time('mysql'),
'level_id' => $morder->membership_id,
'final_amount' => $morder->InitialPayment,
'order_id' => $morder->id ?? null,
'version' => NEXTCLOUD_PLUGIN_VERSION
]);
$config_json = wp_json_encode($config);
if (json_last_error() !== JSON_ERROR_NONE) {
nextcloud_log_error('JSON encoding error', ['error' => json_last_error_msg()]);
return;
}
$saved = update_user_meta($user_id, 'nextcloud_config', $config_json);
// Invalidar caché
nextcloud_invalidate_user_cache($user_id);
if (!$saved) {
nextcloud_log_error('Failed to save user configuration', ['user_id' => $user_id]);
} else {
nextcloud_log_info('Configuration saved successfully', [
'user_id' => $user_id,
'config' => $config
]);
}
}
/**
* Localización de script JS con datos optimizados
*/
function nextcloud_localize_pricing_script() {
// Verificar páginas relevantes
$is_relevant_page = false;
if (function_exists('pmpro_getOption')) {
$checkout_page = pmpro_getOption('checkout_page_slug');
$billing_page = pmpro_getOption('billing_page_slug');
$account_page = pmpro_getOption('account_page_slug');
$is_relevant_page = (
(!empty($checkout_page) && is_page($checkout_page)) ||
(!empty($billing_page) && is_page($billing_page)) ||
(!empty($account_page) && is_page($account_page))
);
}
if (!$is_relevant_page) {
return;
}
// Obtener datos del nivel actual
$level_id = nextcloud_get_current_level_id();
$base_price = 0;
if ($level_id > 0) {
$level = pmpro_getLevel($level_id);
$base_price = $level ? (float)$level->initial_payment : 0;
}
// Datos del usuario actual
$current_storage = '1tb';
$current_suite = '20users';
$used_space_tb = 0;
if (is_user_logged_in()) {
$user_id = get_current_user_id();
$config_json = get_user_meta($user_id, 'nextcloud_config', true);
if ($config_json) {
$config = json_decode($config_json, true);
$current_storage = $config['storage_space'] ?? '1tb';
$current_suite = $config['office_suite'] ?? '20users';
}
$used_space_tb = get_nextcloud_used_space_tb($user_id);
}
// Localizar script
$script_handle = 'simply-snippet-pmpro-dynamic-pricing';
wp_localize_script(
$script_handle,
'nextcloud_pricing',
[
'level_id' => $level_id,
'base_price' => $base_price,
'currency_symbol' => 'R$',
'current_storage' => $current_storage,
'used_space_tb' => $used_space_tb,
'current_suite' => $current_suite,
'debug' => defined('WP_DEBUG') && WP_DEBUG,
'timestamp' => time(),
'ajax_url' => admin_url('admin-ajax.php'),
'version' => NEXTCLOUD_PLUGIN_VERSION
]
);
nextcloud_log_info('Script localized successfully', [
'base_price' => $base_price,
'level_id' => $level_id
]);
}
// ====================================================================
// INICIALIZACIÓN Y HOOKS
// ====================================================================
// Hooks de inicialización múltiples para compatibilidad
add_action('plugins_loaded', 'nextcloud_add_dynamic_fields', 25);
add_action('init', 'nextcloud_add_dynamic_fields', 20);
add_action('wp_loaded', 'nextcloud_add_dynamic_fields', 5);
// Hook principal de modificación de precio
add_filter('pmpro_checkout_level', 'nextcloud_modify_level_pricing', 1);
// Hooks de guardado
add_action('pmpro_after_checkout', 'nextcloud_save_configuration_and_provision', 10, 2);
// Localización de scripts
add_action('wp_enqueue_scripts', 'nextcloud_localize_pricing_script', 30);
// Invalidación de caché en cambios de membresía
add_action('pmpro_after_change_membership_level', function($level_id, $user_id) {
nextcloud_invalidate_user_cache($user_id);
nextcloud_log_info('Cache invalidated on membership change', [
'user_id' => $user_id,
'level_id' => $level_id
]);
}, 10, 2);
// Indicador de estado en admin bar
add_action('admin_bar_menu', function($wp_admin_bar) {
if (!current_user_can('manage_options')) return;
$dependencies_ok = nextcloud_check_dependencies();
$status = $dependencies_ok ? '✅' : '❌';
$wp_admin_bar->add_node([
'id' => 'nextcloud-dynamic-status',
'title' => "PMPro Dynamic {$status}",
'href' => admin_url('plugins.php'),
'meta' => ['title' => "PMPro Dynamic Status"]
]);
}, 100);
nextcloud_log_info('PMPro Dynamic Pricing snippet loaded successfully', [
'version' => NEXTCLOUD_PLUGIN_VERSION,
'php_version' => PHP_VERSION
]);

View File

@ -0,0 +1,7 @@
{
"description": "Custom Simple Accordion",
"last_updated": "2025-08-24 20:25:32",
"active": true,
"hooks": [],
"load_priority": 10
}

View File

@ -0,0 +1,55 @@
<?php
// Custom Simple Accordion
// Shortcode: [simple_accordion]
// Uso:
// [simple_accordion]
// [accordion_item title="Nextcloud TI" icon="dashicons-groups" open="true"]
// [accordion_item title="Nextcloud Solo" icon="dashicons-users"]
// [/simple_accordion]
function simple_accordion_shortcode($atts, $content = null) {
ob_start();
?>
<div class="simple-accordion">
<?php echo do_shortcode($content); ?>
</div>
<?php
return ob_get_clean();
}
add_shortcode('simple_accordion', 'simple_accordion_shortcode');
function accordion_item_shortcode($atts, $content = null) {
$atts = shortcode_atts(array(
'title' => '',
'icon' => 'dashicons-menu',
'open' => 'false',
), $atts, 'accordion_item');
$title = $atts['title'];
$icon = $atts['icon'];
$is_open = ($atts['open'] === 'true' || $atts['open'] === '1');
$header_class = $is_open ? 'accordion-header active' : 'accordion-header';
$content_class = $is_open ? 'accordion-content active' : 'accordion-content';
$aria_expanded = $is_open ? 'true' : 'false';
ob_start();
?>
<div class="accordion-item">
<div class="<?php echo esc_attr($header_class); ?>" tabindex="0" role="button" aria-expanded="<?php echo esc_attr($aria_expanded); ?>">
<div class="accordion-title-wrapper">
<span class="dashicons <?php echo esc_attr($icon); ?> accordion-icon-left"></span>
<span class="accordion-title"><?php echo esc_html($title); ?></span>
</div>
<span class="accordion-icon">+</span>
</div>
<div class="<?php echo esc_attr($content_class); ?>">
<div class="accordion-body">
<?php echo do_shortcode($content); ?>
</div>
</div>
</div>
<?php
return ob_get_clean();
}
add_shortcode('accordion_item', 'accordion_item_shortcode');

View File

@ -0,0 +1,14 @@
{
"description": "Configure SMTP settings",
"last_updated": "2025-08-15 03:24:55",
"active": true,
"hooks": {
"phpmailer_init": {
"type": "action",
"priority": 10,
"accepted_args": 1,
"auto_detected": true
}
},
"load_priority": 10
}

View File

@ -0,0 +1,16 @@
<?php
// Configure SMTP settings
if (!defined('ABSPATH')) exit;
function setup_phpmailer_init( $phpmailer ) {
$phpmailer->IsSMTP();
$phpmailer->Host = 'smtp-relay.sendinblue.com';
$phpmailer->Port = 587;
$phpmailer->SMTPSecure = 'tls';
$phpmailer->SMTPAuth = true;
$phpmailer->Username = 'jdavidcamejo@gmail.com';
$phpmailer->Password = 'Ij8DcFwLxmZM6ytX';
$phpmailer->From = 'web@brasdrive.com.br';
$phpmailer->FromName = 'Brasdrive';
}
add_action( 'phpmailer_init', 'setup_phpmailer_init' );

View File

@ -0,0 +1,26 @@
{
"description": "Agregar scripts y estilos en WP",
"last_updated": "2025-09-29 04:38:23",
"active": true,
"hooks": {
"wp_head": {
"type": "action",
"priority": 10,
"accepted_args": 1,
"auto_detected": true
},
"wp_footer": {
"type": "action",
"priority": 10,
"accepted_args": 1,
"auto_detected": true
},
"wp_enqueue_scripts": {
"type": "action",
"priority": 10,
"accepted_args": 1,
"auto_detected": true
}
},
"load_priority": 10
}

View File

@ -0,0 +1,382 @@
<?php
/**
* Theme Scripts - PMPro Nextcloud Banda Integration SINCRONIZADO v2.8.0
*
* Nombre del archivo: theme-scripts.php
*
* RESPONSABILIDAD: Manejo de handles de script y localización para Simply Code
* CORREGIDO: Sincronización completa con el sistema de precios, inyección 'before'
* MEJORADO: Sanitización defensiva, control de race conditions, logging mejorado
*
* @version 2.8.0
*/
if (!defined('ABSPATH')) {
exit('Acceso directo no permitido');
}
// CORREGIDO: Usar la misma constante que el archivo principal
if (!defined('NEXTCLOUD_BANDA_BASE_PRICE')) {
define('NEXTCLOUD_BANDA_BASE_PRICE', 70.00);
}
// Función de normalización de configuración (backup/fallback)
if (!function_exists('normalize_banda_config')) {
function normalize_banda_config($config_data) {
if (!is_array($config_data)) {
return [
'storage_space' => '1tb',
'num_users' => 2,
'payment_frequency' => 'monthly'
];
}
$storage_space = sanitize_text_field($config_data['storage_space'] ?? '1tb');
$valid_storage = ['1tb','2tb','3tb','4tb','5tb','6tb','7tb','8tb','9tb','10tb','15tb','20tb'];
if (!in_array($storage_space, $valid_storage, true)) {
$storage_space = '1tb';
}
$num_users = max(2, min(20, intval($config_data['num_users'] ?? 2)));
$payment_frequency = sanitize_text_field($config_data['payment_frequency'] ?? 'monthly');
$valid_frequencies = ['monthly','semiannual','annual','biennial','triennial','quadrennial','quinquennial'];
if (!in_array($payment_frequency, $valid_frequencies, true)) {
$payment_frequency = 'monthly';
}
return [
'storage_space' => $storage_space,
'num_users' => $num_users,
'payment_frequency' => $payment_frequency
];
}
}
// Detecta posibles handles de script
function banda_detect_script_handles() {
global $wp_scripts;
$possible_handles = [
'simply-snippet-nextcloud-banda-dynamic-pricing',
'simply-code-nextcloud-banda-dynamic-pricing',
'nextcloud-banda-dynamic-pricing',
'banda-dynamic-pricing',
'simply-snippet-banda-pricing',
'simply-code-banda-pricing',
'simply-snippet-nextcloud-banda',
'simply-code-nextcloud-banda',
'pmpro-banda-pricing',
'banda-pricing-script'
];
$detected_handles = [];
if (isset($wp_scripts->registered)) {
foreach ($wp_scripts->registered as $handle => $script) {
if (stripos($handle, 'banda') !== false ||
stripos($handle, 'nextcloud') !== false ||
stripos($handle, 'pricing') !== false ||
(stripos($handle, 'simply') !== false && stripos($handle, 'snippet') !== false)) {
$detected_handles[] = $handle;
}
}
}
$all_handles = array_unique(array_merge($possible_handles, $detected_handles));
banda_theme_log('Script handles detection completed', [
'possible_count' => count($possible_handles),
'detected_count' => count($detected_handles),
'total_handles' => count($all_handles),
'detected_handles' => array_slice($detected_handles, 0, 5)
]);
return $all_handles;
}
// Función principal de localización mejorada - CORREGIDA
function banda_localize_pricing_script_improved() {
if (!function_exists('pmpro_getOption')) {
banda_theme_log('PMPro functions not available, skipping localization');
return;
}
$checkout_page_id = pmpro_getOption('checkout_page_id');
$account_page_id = pmpro_getOption('account_page_id');
$is_relevant_page = (
is_page($checkout_page_id) ||
is_page($account_page_id) ||
!empty($_GET['level'])
);
if (!$is_relevant_page) {
banda_theme_log('Not on relevant page, skipping localization');
return;
}
$level_id = 0;
if (!empty($_GET['level'])) {
$level_id = (int)sanitize_text_field($_GET['level']);
} elseif (!empty($_GET['pmpro_level'])) {
$level_id = (int)sanitize_text_field($_GET['pmpro_level']);
} elseif (function_exists('nextcloud_banda_get_current_level_id')) {
$level_id = nextcloud_banda_get_current_level_id();
}
$allowed_levels = function_exists('nextcloud_banda_get_config')
? nextcloud_banda_get_config('allowed_levels')
: [2];
if (!in_array($level_id, $allowed_levels, true)) {
banda_theme_log('Level not allowed for localization', [
'level_id' => $level_id,
'allowed_levels' => $allowed_levels,
]);
return;
}
$base_price = NEXTCLOUD_BANDA_BASE_PRICE;
if ($level_id > 0) {
$level = pmpro_getLevel($level_id);
if ($level && !empty($level->initial_payment) && $level->initial_payment > 0) {
$base_price = (float) $level->initial_payment;
}
}
$current_storage = '1tb';
$current_users = 2;
$current_frequency = 'monthly';
$has_previous_config = false;
$used_space_tb = 0;
$next_payment_date = null;
$has_active_membership = false;
$current_subscription = null;
$current_price_paid = 0;
$last_credit_value = 0;
if (is_user_logged_in()) {
$user_id = get_current_user_id();
if (
function_exists('nextcloud_banda_get_next_payment_info') &&
function_exists('nextcloud_banda_get_user_real_config_improved')
) {
$user_levels = pmpro_getMembershipLevelsForUser($user_id);
if (!empty($user_levels)) {
foreach ($user_levels as $l) {
if (in_array((int) $l->id, $allowed_levels, true)) {
$cycle_info = nextcloud_banda_get_next_payment_info($user_id);
if ($cycle_info && !empty($cycle_info['cycle_end']) && $cycle_info['cycle_end'] > time()) {
$has_active_membership = true;
break;
}
}
}
}
if ($has_active_membership) {
$level = pmpro_getMembershipLevelForUser($user_id);
if ($level) {
$real_config = nextcloud_banda_get_user_real_config_improved($user_id, $level);
if (!empty($real_config) && $real_config['source'] !== 'defaults_no_active_membership') {
$current_storage = sanitize_text_field($real_config['storage_space'] ?? '1tb');
$current_users = max(2, min(20, intval($real_config['num_users'] ?? 2)));
$current_frequency = sanitize_text_field($real_config['payment_frequency'] ?? 'monthly');
$has_previous_config = true;
$current_price_paid = !empty($real_config['current_cycle_amount'])
? (float) $real_config['current_cycle_amount']
: ((float) $level->initial_payment);
$last_credit_value = !empty($real_config['last_proration_credit'])
? (float) $real_config['last_proration_credit']
: 0.0;
}
}
if ($level) {
$cycle_number = (int) ($level->cycle_number ?? 1);
$cycle_period = (string) ($level->cycle_period ?? 'Month');
$derived_frequency = function_exists('nextcloud_banda_derive_frequency_from_cycle')
? nextcloud_banda_derive_frequency_from_cycle($cycle_number, $cycle_period)
: 'monthly';
$cycle_label = function_exists('nextcloud_banda_map_cycle_label')
? nextcloud_banda_map_cycle_label($cycle_number, $cycle_period)
: 'Mensal';
$current_subscription = [
'storage_space' => $current_storage,
'num_users' => $current_users,
'payment_frequency' => $derived_frequency,
'cycle_label' => $cycle_label,
'cycle_number' => $cycle_number,
'cycle_period' => $cycle_period,
'final_amount' => !empty($level->initial_payment) ? (float) $level->initial_payment : 0,
'current_price_paid' => $current_price_paid,
'last_credit_value' => $last_credit_value,
'subscription_end_date' => (!empty($level->enddate) && $level->enddate !== '0000-00-00 00:00:00') ? $level->enddate : null,
'subscription_start_date' => !empty($level->startdate) ? $level->startdate : null,
];
}
if (function_exists('nextcloud_banda_get_used_space_tb')) {
$used_space_tb = nextcloud_banda_get_used_space_tb($user_id);
}
$cycle_info = nextcloud_banda_get_next_payment_info($user_id);
if ($cycle_info && !empty($cycle_info['next_payment_date'])) {
$next_payment_date = date('c', (int) $cycle_info['next_payment_date']);
}
}
}
}
$configs_available = function_exists('nextcloud_banda_get_config');
$price_per_tb = $configs_available ? nextcloud_banda_get_config('price_per_tb') : 70.00;
$price_per_user = $configs_available ? nextcloud_banda_get_config('price_per_additional_user') : 10.00;
$base_users_included = $configs_available ? nextcloud_banda_get_config('base_users_included') : 2;
$base_storage_tb = $configs_available ? nextcloud_banda_get_config('base_storage_included') : 1;
$frequency_multipliers = $configs_available
? nextcloud_banda_get_config('frequency_multipliers')
: [
'monthly' => 1.0,
'semiannual' => 5.7,
'annual' => 10.8,
'biennial' => 20.4,
'triennial' => 28.8,
'quadrennial' => 36.0,
'quinquennial'=> 42.0,
];
$localization = [
'level_id' => $level_id,
'base_price' => $base_price,
'currency_symbol' => 'R$',
'price_per_tb' => (float) $price_per_tb,
'price_per_user' => (float) $price_per_user,
'base_users_included' => (int) $base_users_included,
'base_storage_included' => (int) $base_storage_tb,
'current_storage' => $current_storage,
'current_users' => $current_users,
'current_frequency' => $current_frequency,
'has_previous_config' => (bool) $has_previous_config,
'hasActiveMembership' => (bool) $has_active_membership,
'current_subscription_data'=> $current_subscription,
'used_space_tb' => (float) $used_space_tb,
'next_payment_date' => $next_payment_date,
'frequency_multipliers' => $frequency_multipliers,
'frequency_days' => [
'monthly' => 30,
'semiannual' => 182,
'annual' => 365,
'biennial' => 730,
'triennial' => 1095,
'quadrennial' => 1460,
'quinquennial'=> 1825,
],
'debug' => defined('WP_DEBUG') && WP_DEBUG,
'version' => defined('NEXTCLOUD_BANDA_PLUGIN_VERSION') ? NEXTCLOUD_BANDA_PLUGIN_VERSION : '2.8.0',
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('nextcloud_banda_proration'),
'base_price_constant' => NEXTCLOUD_BANDA_BASE_PRICE,
];
$json = wp_json_encode($localization, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($json === false) {
banda_theme_log('Failed to json_encode localization data');
return;
}
$inline_script = "window.nextcloud_banda_pricing = {$json};";
$handles = banda_detect_script_handles();
$localized = false;
foreach ($handles as $handle) {
if (wp_script_is($handle, 'registered')) {
wp_add_inline_script($handle, $inline_script, 'before');
banda_theme_log("Inline localization injected before handle: {$handle}");
$localized = true;
break;
}
}
// Fallbacks si no se pudo inyectar 'before' en ningún handle
if (!$localized) {
// Fallback 1: wp_head temprano
add_action('wp_head', function() use ($inline_script) {
echo "<script>{$inline_script}</script>\n";
}, 1);
// Fallback 2: wp_footer como backup
add_action('wp_footer', function() use ($inline_script) {
echo "<script>if (typeof window.nextcloud_banda_pricing === 'undefined') { {$inline_script} console.log('[PMPro Banda Theme] Localization injected via footer fallback'); }</script>\n";
}, 5);
banda_theme_log('Localization injected via inline head/footer fallback', []);
} else {
banda_theme_log('Localization injected via wp_add_inline_script(before)', ['handles_checked' => count($handles)]);
}
banda_theme_log('Localization process completed', [
'method' => $localized ? 'inline_before_handle' : 'inline_head_footer_fallback',
'handles_checked' => count($handles),
'base_price' => $base_price,
'base_price_constant' => NEXTCLOUD_BANDA_BASE_PRICE,
'has_subscription_data' => !empty($current_subscription_data)
]);
}
// Hooks para ejecutar la localización - MEJORADOS
add_action('wp_enqueue_scripts', 'banda_enqueue_banda_assets', 20);
function banda_enqueue_banda_assets() {
wp_enqueue_style('dashicons');
banda_localize_pricing_script_improved();
}
// Fallback adicional para asegurar ejecución
add_action('wp_head', function() {
if (!did_action('banda_localize_pricing_script_improved')) {
banda_localize_pricing_script_improved();
}
}, 999);
// Función de logging simplificada
function banda_theme_log($message, $context = []) {
if (function_exists('nextcloud_banda_log_info')) {
nextcloud_banda_log_info('[Theme Scripts] ' . $message, $context);
} elseif (defined('WP_DEBUG') && WP_DEBUG) {
$log_message = '[Banda Theme] ' . $message;
if (!empty($context)) {
$log_message .= ' | ' . wp_json_encode($context, JSON_UNESCAPED_UNICODE);
}
error_log($log_message);
}
}
// Enqueue de scripts personalizados adicionales
function enqueue_custom_contact_form_scripts() {
wp_enqueue_script('custom-contact-form', get_template_directory_uri() . '/js/custom-contact-form.js', array('jquery'), '1.0', true);
wp_localize_script('custom-contact-form', 'customContactForm', array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('custom_contact_form_nonce')
));
}
add_action('wp_enqueue_scripts', 'enqueue_custom_contact_form_scripts');
// Log de inicialización
banda_theme_log('Theme Scripts loaded successfully - SYNCHRONIZED VERSION', [
'version' => '2.8.0',
'base_price_constant' => NEXTCLOUD_BANDA_BASE_PRICE,
'functions_available' => [
'normalize_banda_config' => function_exists('normalize_banda_config'),
'pmpro_functions' => function_exists('pmpro_getOption'),
'nextcloud_banda_functions' => function_exists('nextcloud_banda_get_config')
]
]);

11
templates/class.php Normal file
View File

@ -0,0 +1,11 @@
<?php
// @description Clase personalizada
class Mi_Clase_Personalizada {
public function __construct() {
// Inicialización
}
public function mi_metodo() {
// Código del método
}
}

View File

@ -0,0 +1,6 @@
<?php
// @description Configuración personalizada
define('MI_CONSTANTE', 'valor');
// Opciones personalizadas
add_option('mi_opcion', 'valor_predeterminado');

5
templates/function.php Normal file
View File

@ -0,0 +1,5 @@
<?php
// @description Función personalizada
function mi_funcion_personalizada() {
// Tu código aquí
}

5
templates/hook.php Normal file
View File

@ -0,0 +1,5 @@
<?php
// @description Hook de WordPress
add_action('init', function() {
// Tu código aquí
});