Criando um plugin de modo de manutenção para WordPress

Criando um plugin de modo de manutenção para WordPress

TL; DR

Muitos desenvolvedores de sites que utilizam, ou não, a plataforma WordPress têm a necessidade, vez ou outra, de implementar em seus projetos um sistema que permita suspender temporariamente o acesso público ao seu site, seja para inauguração de alguma novidade, para modificação de alguma parte do conteúdo ou até mesmo para alteração do código do site, mesmo esse último não sendo aconselhável em nenhuma hipótese.

Quando comecei a trabalhar com WordPress usava plugins de terceiros que me disponibilizavam tal funcionalidade, mas os projetos foram crescendo e esses mesmos plugins não se adequavam mas as minhas necessidade. Foi então que precisei elaborar o meu próprio plugin.

No decorrer do artigo você vai criar um plugin que permita implantar um modo de manutenção para wordpress, nada muito complicado, é uma versão básica do plugin que utilizo, mas que exige um conhecimento de básico para intermediário de WordPress. É até uma boa forma de estudar a elaboração de plugins para WordPress.

Source do projeto no GitHub

O que faremos:

Desenvolveremos um plugin WordPress chamado “custom-maintenance” seguindo os Coding Standards do WordPress. Também usaremos algo que acho muito vantajoso que é a orientação a objetos, que dentre os seus muitos benefícios ajuda a evitar conflitos de nomes.

O plugin terá uma área administrativa onde serão feitas as configurações do modo de manutenção e permitirá que uma página nomeada como 503.php dentro da pasta do seu tema possa ser usado como exibição quando o modo de manutenção estiver ativo.

Organização de arquivos e diretórios

Para uma fácil organização e entendimento desse projeto, até porque esse pode ser seu primeiro plugin, criaremos uma estrutura muito simples e sem muito mistério. Nosso plugin contatá com 3 arquivos abaixo dentro uma pasta chamada custom-maintenance que deverá ser criada dentro do diretório de plugins, normalmente wp-content/plugins/.

  • custom-maintenance.php – Arquivo principal do plugin
  • uninstall.php – Arquivo executado quando o plugin é removido
  • 503-default.php – Modelo de página de manutenção

Mão na massa

Como eu disse no começo do artigo que usaríamos orientação a objeto, teremos que criar uma classe que liderá com as funcionalidades do plugin. Como eu quero também que apenas uma instancia dessa classe seja criado em toda a rotina de execução adotaremos um padrão de design chamado singleton.

Vamos começar a colocar a mão na massa editando o arquivo custom-maintenance.php. Vou colocar abaixo a estrutura inicial que temos que ter.

Ponto de partida do arquivo custom-maintenance.php
<?php
/*
 * Plugin Name: WordPress Custom Maintenance
 * Plugin URI: http://URI_Of_Page_Describing_Plugin_and_Updates
 * Description: Add a page that prevents your site's content view. Ideal to report a scheduled maintenance or coming soon page. 
 * Version: 0.0.1
 * Author: Miguel Müller
 * Author URI: https://github.com/miguelsmuller
 * License: GPLv2 or later
 * License URI: http://www.gnu.org/licenses/gpl-2.0.html
 */

if ( ! defined( 'ABSPATH' ) ) exit;

if ( ! class_exists( 'Custom_Maintenance' ) ) {
class Custom_Maintenance
{
    /**
     * Instance of this class.
     *
     * @var object
     */
    private static $instance = null;

    /**
     * Return an instance of this class.
     *
     * @return object A single instance of this class.
     */
    public static function get_instance() {
        if ( null == self::$instance ) {
            self::$instance = new self;
        }
        return self::$instance;
    }

    /**
     * Initialize the plugin public actions.
     */
    private function __construct() {

    }
}

add_action( 'plugins_loaded', array( 'Custom_Maintenance', 'get_instance' ), 0 );
}
VER CÓDIGO COMPLETO

Se entrar na listagem de plugins do nosso painel administrativo nesse momento já teremos o primeiro resultado, nosso plugin que aplica um modo de manutenção para wordpress já está aparecendo na lista. \o/

Armazenando e gerenciando as configurações

Nossa plugin vai precisar armazenar opções no banco de dados de modo que ele possa funcionar. O WordPress oferece as funções add_optionget_optionupdate_option que facilitam bastante o nosso trabalho.

Vamos então criar um método chamado do_plugin_settings que vai verificar a existência e trabalhar com a opção maintenance_settings. As informações que vierem dessa opção nós iremos salvar dentro de uma propriedade da classe que receberá o mesmo nome. Esse método que criaremos vai ser chamado dentro do construtor da nossa classe. Segue  o código:

Declarando uma propriedade da classe
/* Plugin Settings
 *
 * @var array
 */
protected $maintenance_settings;
VER CÓDIGO COMPLETO
Chamando o método do_plugin_settings() no construtor
$this->do_plugin_settings();
VER CÓDIGO COMPLETO
Método do_plugin_settings()
/**
 * Initializes the plugins options
 */
public function do_plugin_settings() {
    if( false == get_option( 'maintenance_settings' )) {
        add_option( 'maintenance_settings' );
        $default = array(
            'status'           => FALSE,
            'description'      => '', // Why the maintenance mode is active
            'time_activated'   => '', // Time that has been activated
            'duration_days'    => '', // Days suspended
            'duration_hours'   => '', // Hours suspended
            'duration_minutes' => '', // Minutos suspended
            'url_allowed'      => '',
            'role_allow_front' => '',
            'role_allow_back'  => ''
        );
        update_option( 'maintenance_settings', $default );
    }
    $this->maintenance_settings = get_option( 'maintenance_settings' );
    if (!isset($this->maintenance_settings['status'])) $this->maintenance_settings['status'] = FALSE;
}
VER CÓDIGO COMPLETO

Nesse momento você já pode deixar o seu plugin ativo. E como vamos gerenciar o modo de manutenção? Vamos fazer isso através uma página de configuração acessível através do menu lateral do WordPress. Para isso faremos uso de 2 hooks, o admin_init e o admin_menu.

Ambos os ganchos serão chamados dentro do construtor da classe. No admin_init usaremos o Settings API do WordPress para criar uma formulário que será exibido na página de administração e no admin_menu faremos com que essa página de administração seja exibida.

Inserindo métodos aos hooks admin_init e admin_menu dentro do construtor
add_action( 'admin_init', array( &$this, 'admin_init' ));
add_action( 'admin_menu', array( &$this, 'admin_menu' ));
VER CÓDIGO COMPLETO
Métodos para criação da página invocados no construtor
/**
 * Create a form to be used for theme configuration
 */
function admin_init(){
    add_settings_section(
        'section_maintenance',
        'Configura os detalhes do modo de manutenção:',
        '__return_false',
        'custom-maintenance'
    );

    add_settings_field(
        'status',
        'Habilitar modo de manunteção:',
        array( &$this, 'html_input_status' ),
        'custom-maintenance',
        'section_maintenance'
    );

    add_settings_field(
        'description',
        'Motivo do modo de manunteção:',
        array( &$this, 'html_input_description' ),
        'custom-maintenance',
        'section_maintenance'
    );

    add_settings_field(
        'url_allowed',
        'As seguintes páginas terão acesso liberado:',
        array( &$this, 'html_input_url_allowed' ),
        'custom-maintenance',
        'section_maintenance'
    );

    add_settings_field(
        'role_allow',
        'Quem pode acessar:',
        array( &$this, 'html_input_role_allow' ),
        'custom-maintenance',
        'section_maintenance'
    );

    register_setting(
        'custom-maintenance',
        'maintenance_settings'
    );
}

public function html_input_status(){
    if ($this->maintenance_settings['status'] == TRUE) :
        $return    = $this->calc_time_maintenance();

        $message = sprintf( 'O modo de manuntenção se irá terminanr no dia %s', $return['return-date'] );
        echo ("<p>$message</p><br/>");
    endif;

    $days  = $this->maintenance_settings['status'] == TRUE ? $return['remaining-array']['days'] : '1';
    $hours = $this->maintenance_settings['status'] == TRUE ? $return['remaining-array']['hours'] : '0';
    $mins  = $this->maintenance_settings['status'] == TRUE ? $return['remaining-array']['mins'] : '0';
    ?>

    <input type="hidden" name="maintenance_settings[time_activated]" value="<?php echo current_time('timestamp'); ?>">

    <label>
        <input type="checkbox" id="status" name="maintenance_settings[status]" value="TRUE" <?php checked( 'TRUE', $this->maintenance_settings['status'] ) ?> /> Quero habilitar
    </label>

    <br/>
    <table>
        <tbody>
            <tr>
                <td><?php _e('Back in:'); ?></td>
                <td><input type="text" id="duration_days" name="maintenance_settings[duration_days]" value="<?php echo $days; ?>" size="4" maxlength="5"> <label for="duration_days">Dias</label></td>
                <td><input type="text" id="duration_hours" name="maintenance_settings[duration_hours]" value="<?php echo $hours; ?>" size="4" maxlength="5"> <label for="duration_hours">Horas</label></td>
                <td><input type="text" id="duration_minutes" name="maintenance_settings[duration_minutes]" value="<?php echo $mins; ?>" size="4" maxlength="5"> <label for="duration_minutes">Minutos</label></td>
            </tr>
        </tbody>
    </table>
    <?php
}

public function html_input_description(){
    $html = '<textarea id="description" name="maintenance_settings[description]" cols="80" rows="5" class="large-text">'.$this->maintenance_settings['description'].'</textarea>';
    echo $html;
}

public function html_input_url_allowed(){
    $html = '<textarea id="url_allowed" name="maintenance_settings[url_allowed]" cols="80" rows="5" class="large-text">'.$this->maintenance_settings['url_allowed'].'</textarea>';
    $html .= '<br/>Digite os caminhos que devem estar acessíveis mesmo em modo de manutenção. Separe os vários caminhos com quebras de linha.<br/>Exemplo: Se você quer liberar acesso á pagina http://site.com/sobre/, você deve digitar /sobre/.<br/>Dica: Se você quiser liberar acesso a página inicial digite [HOME].';
    echo $html;
}

public function html_input_role_allow(){
    //INPUT FOR ALLOW BACK
    $html = '<label>Acesso ao painel administrativo: ';
    $html .= ' <select id="role_allow_back" name="maintenance_settings[role_allow_back]">
                <option value="manage_options" ' . selected( $this->maintenance_settings['role_allow_back'], 'manage_options', false) . '>Ninguém</option>
                <option value="manage_categories" ' . selected( $this->maintenance_settings['role_allow_back'], 'manage_categories', false) . '>Editor</option>
                <option value="publish_posts" ' . selected( $this->maintenance_settings['role_allow_back'], 'publish_posts', false) . '>Autor</option>
                <option value="edit_posts" ' . selected( $this->maintenance_settings['role_allow_back'], 'edit_posts', false) . '>Colaborador</option>
                <option value="read" ' . selected( $this->maintenance_settings['role_allow_back'], 'read', false) . '>Visitante</option>
            </select>';
    $html .= '</label><br />';

    //INPUT FOR ALLOW FRONT
    $html .= '<label>Acesso ao site público: ';
    $html .= ' <select id="role_allow_front" name="maintenance_settings[role_allow_front]">
                <option value="manage_options" ' . selected( $this->maintenance_settings['role_allow_front'], 'manage_options', false) . '>Ninguém</option>
                <option value="manage_categories" ' . selected( $this->maintenance_settings['role_allow_front'], 'manage_categories', false) . '>Editor</option>
                <option value="publish_posts" ' . selected( $this->maintenance_settings['role_allow_front'], 'publish_posts', false) . '>Autor</option>
                <option value="edit_posts" ' . selected( $this->maintenance_settings['role_allow_front'], 'edit_posts', false) . '>Colaborador</option>
                <option value="read" ' . selected( $this->maintenance_settings['role_allow_front'], 'read', false) . '>Visitante</option>
            </select>';
    $html .= '</label><br />';
    echo $html;
}


/**
 * Set an administrative page accessed by the menu
 */
function admin_menu(){
    add_submenu_page(
        'options-general.php',
        'Modo de manunteção',
        'Modo de manutenção',
        'administrator',
        'custom-maintenance',
        array( &$this, 'html_form_settings' )
    );
}

public function html_form_settings(){
?>
    <div class="wrap">
        <div id="icon-options-general" class="icon32"></div>
        <h2><?php _e('General Settings'); ?></h2>
        <form method="post" action="options.php">
            <?php
            settings_fields( 'custom-maintenance' );
            do_settings_sections( 'custom-maintenance' );
            submit_button();
            ?>
        </form>
    </div>
<?php
}
VER CÓDIGO COMPLETO

Até agora tudo certo. Quando definirmos que o plugin está ativo através do nosso painel administrativo, informamos também a quantidade de dias, horas e minutos que ele permanecerá inativo. Temos também um campo oculto que contém o timespamp com o momento em que a página foi acessada.

TIMESTAMP é um formato de tempo que apresenta os segundos que se passaram desde 1970 até agora. É um número de 11 algarismos (até agora)

É através dessas informações que saberemos quando se encerrará o período de manutenção. Os nossos cálculos serão feitos através das 2 funções abaixo.

O método calc_time_maintenance vai nos retornar um array contendo uma string com o dia em um formato completo,  uma string com os segundos restantes e um array contendo dias, horas e segundos para o  se encerrar o período de manutenção.

O método calc_separate_time vai fatorar os segundos que faltam para o período de manutenção, vai converter e construir um array com dias, horas e minutos para o fim do período de manunteção.

Método calc_time_maintenance()
/**
 * 
 */
public function calc_time_maintenance(){
    // How long will it stay off in seconds
    $time_duration = 0;
    $time_duration += intval($this->maintenance_settings['duration_days']) * 24 * 60;
    $time_duration += intval($this->maintenance_settings['duration_hours']) * 60;
    $time_duration += intval($this->maintenance_settings['duration_minutes']);
    $time_duration = intval($time_duration * 60);

    // Timestamp of time activated, time finished, time current e time remaining
    $time_activated = intval($this->maintenance_settings['time_activated']);
    $time_finished  = intval($time_activated + $time_duration);
    $time_current   = current_time('timestamp');
    $time_remaining = $time_finished - $time_current;

    // Format the date in the format defined by the system
    $return_day  = date_i18n( get_option('date_format'), $time_finished );
    $return_time = date_i18n( get_option('time_format'), $time_finished );
    $return_date = $return_day . ' ' . $return_time;

    $time_calculated = $this->calc_separate_time($time_remaining);

    return array(
        'return-date'       => $return_date,
        'remaining-seconds' => $time_remaining,
        'remaining-array'   => $time_calculated,
    );
}
VER CÓDIGO COMPLETO
Método calc_separate_time()
/**
 * Calculates the days, hours and minutes remaining based on the number of seconds
 *
 * @return array Array containing the values of days, hours and minutes remaining
 */
private function calc_separate_time($seconds){
    $minutes = round(($seconds/(60)), 0);
    $minutes = intval($minutes);

    $vals_arr = array(
        'days'  => (int) ($minutes / (24*60) ),
        'hours' => $minutes / 60 % 24,
        'mins'  => $minutes % 60
    );

    $return_arr = array();
    $is_added = false;
    
    foreach ($vals_arr as $unit => $amount) {
        $return_arr[$unit] = 0;

        if ( ($amount > 0) || $is_added ) {
            $is_added          = true;
            $return_arr[$unit] = $amount;
        }
    }
    return $return_arr;
}
VER CÓDIGO COMPLETO

Aplicando o modo manutenção

Agora que já temos uma forma de administrar as opções do plugin, precisamos realmente botar ele pra funcionar. A lógica é a seguinte: Se o plugin estiver ativo, ou seja, o item “status” dentro da opção maintenance_settings estiver TRUE, vamos mostrar a página de manutenção. Vamos usar o hook wp_loaded fazer a mágica acontecer.

O método apply_maintenance_mode() não interrompe o processamento em alguns casos como você pode notar logo no começo do método. Assim como utilizaremos os métodos check_url_allowed() para verificar as URLs liberadas, user_allow() para verificar se o visitante tem acesso e display_maintenance_page() para fazer a exibição da página em si.

Chamando o método apply_maintenance_mode() no construtor
if ($this->maintenance_settings['status'] === 'TRUE') {
    add_action( 'wp_loaded', array( &$this, 'apply_maintenance_mode' ));
}
VER CÓDIGO COMPLETO
Métodos para aplicação do modo manutenção
/**
 * Method that manages the application of the maintenance mode
 */
function apply_maintenance_mode()
{
    if ( strstr($_SERVER['PHP_SELF'],'wp-login.php')) return;
    if ( strstr($_SERVER['PHP_SELF'], 'wp-admin/admin-ajax.php')) return;
    if ( strstr($_SERVER['PHP_SELF'], 'async-upload.php')) return;
    if ( strstr(htmlspecialchars($_SERVER['REQUEST_URI']), '/plugins/')) return;
    if ( strstr($_SERVER['PHP_SELF'], 'upgrade.php')) return;
    if ( $this->check_url_allowed()) return;

    //Never show maintenance page in wp-admin
    if ( is_admin() || strstr(htmlspecialchars($_SERVER['REQUEST_URI']), '/wp-admin/') ) {
        if ( !is_user_logged_in() ) {
            auth_redirect();
        }
        if ( $this->user_allow('admin') ) {
            return;
        } else {
            $this->display_maintenance_page();
        }
    } else {
        if( $this->user_allow('public') ) {
            return;
        } else {
            $this->display_maintenance_page();
        }
    }
}


/**
 * Checks if a URL is freed from the maintenance mode
 */
function check_url_allowed()
{
    $urlarray = $this->maintenance_settings['url_allowed'];
    $urlarray = preg_replace("/\r|\n/s", ' ', $urlarray); //TRANSFORM BREAK LINES IN SPACE
    $urlarray = explode(' ', $urlarray); //TRANSFORM STRING IN ARRAY
    $oururl = 'http://' . $_SERVER['HTTP_HOST'] . htmlspecialchars($_SERVER['REQUEST_URI']);
    foreach ($urlarray as $expath) {
        if (!empty($expath)) {
            $expath = str_replace(' ', '', $expath);
            if (strpos($oururl, $expath) !== false) return true;
            if ( (strtoupper($expath) == '[HOME]') && ( trailingslashit(get_bloginfo('url')) == trailingslashit($oururl) ) )    return true;
        }
    }
    return false;
}


/**
 * Checks if user can access the site even with him suspended
 */
function user_allow($where)
{
    if ($where == 'public') {
        $optval = $this->maintenance_settings['role_allow_front'];
    } elseif ($where == 'admin') {
        $optval = $this->maintenance_settings['role_allow_back'];
    } else {
        return false;
    }

    if ( $optval == 'manage_options' && current_user_can('manage_options') ) { return true; }
    elseif ( $optval == 'manage_categories' && current_user_can('manage_categories') ) { return true; }
    elseif ( $optval == 'publish_posts' && current_user_can('publish_posts') ) { return true;   }
    elseif ( $optval == 'edit_posts' && current_user_can('edit_posts') ) { return true; }
    elseif ( $optval == 'read' && current_user_can('read') ) { return true; }
    else { return false; }
}


/**
 * Method that displays the maintenance page
 */
function display_maintenance_page()
{
    $time_maintenance = $this->calc_time_maintenance();
    $time_maintenance = $time_maintenance['remaining-seconds'];

    //Define header as unavailable
    header('HTTP/1.1 503 Service Temporarily Unavailable');
    header('Status: 503 Service Temporarily Unavailable');

    if ( $time_maintenance > 1 ) header('Retry-After: ' . $time_maintenance );

    // Check what used in page will be visitor redirect
    $file503 = get_template_directory() . '/503.php';
    if (file_exists($file503) == FALSE) {
        $file503 = dirname(  __FILE__  ) . '/503-default.php';
    }

    // Show page
    include($file503);

    exit();
}
VER CÓDIGO COMPLETO

Página para exibição da mensagem de manutenção

A lógica do nosso plugin define que se houver um arquivo chamado 503.php na pasta raiz do tema ele será usado como mensagem de manutenção, caso contrário ele usará o arquivo 503-default.php na pasta raiz do plugin. O arquivo padrão do plugin também não é um bicho de 7 cabeças. Ele é bem simples.

503 é o código do erro HTTP para service unavailable. Este erro indica que o servidor no momento não pode processar a solicitação gerada pela sua aplicação.

503-default.php
<?php if ( ! defined( 'ABSPATH' ) ) exit; ?>

<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
    <meta charset="<?php bloginfo('charset'); ?>">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title><?php wp_title( '|', true, 'right' ); ?></title>

    <?php wp_head();?>
</head>
<body <?php body_class(); ?>>
    <?php
    $retorno = Custom_Maintenance::calc_time_maintenance();
    $retorno = $retorno['return-date'];

    echo "<p style='text-align: center; display: block; margin-top: 50px;'>O site está em manutenção.<br/>A previsão de retorno é para  $retorno; </p>";
    ?>

    <?php wp_footer(); ?>
</body>
</html>
VER CÓDIGO COMPLETO

Possibilitando uma desinstalação completa

O WordPress tem uma forma bem fácil de lidar com a desinstalação de um plugin. Um simples arquivo chamado uninstall.php é executado quando o processo de desinstalação é solicitado. Como a única informação que criamos foi a opção para armazenar as definições do plugin do banco de dados ela será a única que precisaremos remover.

uninstall.php
<?php
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) exit;

delete_option( 'maintenance_settings' );
VER CÓDIGO COMPLETO

Conclusão

Com esse esquema que apresentei você consegue desenvolver um plugin de modo de manutenção para wordpress que permita suspender o site temporariamente. Se você quiser pode implementar novas funcionalidades. Uma das coisas que eu gosto de fazer nos meus projetos é adicionar um contador regressivo animado na pagina 503.php através de plugin jquery.

Se você se interessar o código melhorado desse plugin está no GitHub da devim tendo como melhoria as strings preparadas para tradução.

Source do projeto no GitHub

Artigos relacionados

Comentários