Hackeando código en Karamelo
La aplicación debe estar ya instalada.
Karamelo está desarrollado usando CakePHP de modo que si aún no conoces este magnífico framework de desarrollo es un buen momento para iniciar, puede iniciar aqui o consultar la documentación oficial. En este documento APP significa el directorio donde Karamelo está instalado (podría ser /var/www/karamelo/app), mientras CAKE es el directorio donde están las librerías del framework (/var/www/karamelo/cake).
CakePHP utiliza el patron de diseño MVC. Resumiendo:
1) El controlador recibe el input ya sea desde el url del navegador o de un formulario y ejecuta el método que le corresponde.
2) El controlador consulta al modelo con el método $this->Model->findAll() si queremos todos los registros de la tabla o $this->Model->find() si buscamos un sólo registro específico con el id de la tabla
3) El resultado de la consulta es transferido del controlador a la vista con el método $this->set('data', $data)
4) La vista se procesa y se integra al layout general del portal en el navegador
En este ejemplo de hacking supondremos que deseamos extender Karamelo agregando una tabla en PostgreSQL llamada "recipes" (recetas) para que los usuarios de Karamelo puedan hacerle el típico CRUD (Create, Read, Update and Delete) desde el Panel de Control y además para que los internautas puedan ver esas recetas en el portal. Nuestra tabla SQL sería como sigue:
CREATE TABLE recipes ( id serial PRIMARY KEY, title varchar(50) NOT NULL, user_id int NOT NULL REFERENCES users(id) ON DELETE CASCADE, recipe text NOT NULL );
Una vez que hemos creado nuestra tabla en la BD debemos crear nuestro modelo para acceder a ella usando el patrón de diseño ActiveRecord. La tabla en la BD debe estar en plural (recipes) y el modelo debe ser en singular (Recipe), en este caso necesitamos crear el archivo recipe.php :
<?php
/**
* Hacking Karamelo
* File: APP/models/recipe.php
*/
class Recipe extends AppModel {
// recipe belongs to user
public $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id',
'fields' => 'id, username'
)
);
// Validate fields before save or update
public $validate = array(
'title' => array(
'rule' => array('minLength', 4),
'message' => 'Title must be at least four characters long',
'allowEmpty' => false,
'required' => true
),
'recipe' => array(
'rule' => array('minLength', 15),
'message' => 'Field must be at least 15 characters long',
'allowEmpty' => false,
'required' => true
),
'user_id' => array(
'rule' => 'numeric',
'allowEmpty' => false,
'on' => 'create', // but not on update
'required' => true
)
);
}
?>
Ahora creamos el controlador RecipesController?:
<?php
/**
* Hacking Karamelo
* GPLv3
* @author Your Name
* @version 0.3
* @package app
*/
// file: app/controllers/recipes_controller.php
App::import('Sanitize');
class RecipesController extends AppController {
public function beforeFilter()
{
parent::beforeFilter();
}
/** === ADMIN METHODS === ***/
public function admin_add()
{
$this->layout = 'admin'; // load APP/views/layouts/admin.ctp
if (isset($this->data['Recipe'])):
Sanitize::clean($this->data['Recipe']); // antihacking line
$this->data['Recipe']['user_id'] = (int) $this->Auth->user('id'); // Auth component gives you current user session data
if ($this->Recipe->save($this->data)):
$this->msgFlash(__('Data saved', true), '/admin/recipes/listing');
endif;
endif;
}
}
?>
Como podemos ver este controlador posee el método admin_add() que es parte de la zona "admin". Para acceder a este método desde nuestro navegador debemos poner en el url http://localhost/admin/recipes/add. Para activar la zona admin de CakePHP la línea:
Configure::write('Routing.admin', 'admin');
en el archivo APP/config/core.php debe estar descomentada.
Nota: Quizás se pregunte que quiere decir:
__('Data saved', true)
La función doble underscore:
__()
de CakePHP traduce la cadena al idioma definido en la variable:
Configure::write('Config.language', $lang);
En este caso el idioma es "es" (español) de modo que CakePHP busca el archivo default.po para traducir el string, en este caso el archivo de traducción contiene:
msgid "Data saved" msgstr "Datos guardados"
El parametro true de la función doble underscore indica que el string traducido no debe ser inmediatemente impreso en pantalla sino devuelto como valor.
El archivo app_controller.php
El método beforeFilter() que se puede usar en los controladores es parte del API de CakePHP y es (como su nombre lo indica) un método que se ejecuta antes de cualquier otro método que llamemos del controlador. Además en CakePHP podemos crear el archivo app_controller.php que hace el rol de "padre" de todos los controladores y hereda sus métodos y atributos a los demás controladores.
Por ejemplo en el archivo app_controller.php de Karamelo está definido el método msgFlash() que simplemente coloca un mensaje flash y redirecciona al usuario a otro método. Como este método msgFlash() está definido en app_controller.php está disponible en cualquier controlador de Karamelo pues lo hereda a sus "hijos". Del mismo modo los helpers y componentes que llamemos en app_controller.php estarán disponibles en cualquier controlador y ya no será necesario llamarlos en cada uno de ellos.
En app_controller.php se ejecuta beforeFilter() (un método que como dijimos se ejecuta antes que todo) este método en app_controller.php llama y configura el Auth componente, es decir el componente que verifica que el usuario está logeado y tiene una sesión válida. El método beforeFilter de app_controller.php define varias cosas, entre ellas:
$this->Auth->authorize = 'controller';
$this->Auth->deny('*'); // we are fascist and all is prohibited!!
La primera indicación le dice a Auth que cada controlador tendrá su propia definición de permisos y que además habrá una segunda prueba de sesión usando el método isAuthorized() que debe regresar true para ejecutar el método del controlador. La segunda línea declara que por default todos los métodos del controlador están prohibidos. De modo que el método:
public function beforeFilter()
{
parent::beforeFilter();
}
en nuestra clase RecipesController? simplemente está llamando al método beforeFilter de app_controller.php para que se haga cargo de los permisos. ¿Simple no?
Ahora debemos crear el directorio APP/views/recipes/ y dentro de él colocar la vista admin_add.ctp para el método admin_add.
<?php
echo $html->addCrumb('Control Panel', '/admin/entries/start');
echo $html->addCrumb(__('Recipes', true), '/admin/recipes/listing');
echo $html->getCrumbs(' > ');
echo $form->create('Recipe');
?>
<fieldset>
<legend><?php __('Add Recipe'); ?></legend>
<?php
echo $form->input('Recipe.title', array('size' => 40, 'maxlength' => 50, 'label'=>__('Title', true))) . '<br />';
echo $form->textarea('Recipe.recipe', array('cols'=>80, 'rows'=>20)).'<br />';
echo $form->error('Recipe.recipe');
echo $form->end(__('Save', true));
?>
</fieldset>
Debemos colocarnos en http://nuestroserver.org/admin/recipes/add, como el método isAuthorized() de app_controller.php indica, sólo si estamos logeados y somos miembros del grupo admin o del grupo profesores podremos ver este url que es lo que hacen las líneas:
if ($this->Auth->user('group_id') == 1 || $this->Auth->user('group_id') == 2 ):
return true;
de otro modo el Auth component nos redireccionará a /users/login para logearnos.
El formulario que hemos creado luce así:
Como vemos en el método admin_add de nuestro controlador RecipesController?, si existen datos de la receta enviados por el formulario:
if (isset($this->data['Recipe']))
se hace un Sanitize por seguridad, se validan los datos usando el array $validate del modelo recipe.php si los campos están vacíos se verán los mensajes de error de validación, intente guardar con los campos vacíos. Si los datos están correctos se guardan los con el método save() del modelo y luego se usa el método msgFlash() para colocar un mensaje traducido y redireccionar a /admin/recipes/listing, esté metodo aún no lo tenemos en el controlador, asi pues debemos agregar este método admin_listing a nuestro RecipesController?:
public function admin_listing()
{
$this->layout = 'admin';
$conditions = array('Recipe.user_id' => $this->Auth->user('id'));
$fields = array('Recipe.id', 'Recipe.title', 'Recipe.recipe');
$order = 'Recipe.title';
$this->set('data', $this->Recipe->findAll($conditions, $fields, $order));
}
Y guardar la vista APP/views/recipes/admin_listing.ctp que le corresponde:
<?php
echo $html->addCrumb('Control Panel', '/admin/entries/start');
echo $html->getCrumbs(' / ');
echo $html->div('title_section', __('Recipes', true));
echo $html->para(null, $html->link($html->image('actions/new.png', array('alt'=>__('Add new', true), 'title'=>__('Add new', true))), '/admin/recipes/add', null, null, false));
?>
<table class="tbadmin">
<?php
$th = array(__('Edit', true), __('Title', true), __('Delete', true));
echo $html->tableHeaders($th, array('style'=>'text-align:center'));
foreach ($data as $val):
$tr = array(
$gags->sendEdit($val['Recipe']['id'], 'recipes'),
$val['Recipe']['title'],
$gags->confirmDel($val['Recipe']['id'], 'recipes')
);
echo $html->tableCells($tr, array("class"=>"altRow", "onmouseover"=>"this.className='highlight'", "onmouseout"=>"this.className='altRow'"),
array('class'=>'evenRow',"onmouseover"=>"this.className='highlight'", "onmouseout"=>"this.className='evenRow'"));
endforeach;
?>
</table>
hay que hacer notar que en esta vista estamos usando el helper Gags de Karamelo, que simplemta hace pas sencillo pintar los botones de edición y borrado de nuestra pantalla /admin/recipes/listing así que debemos incluirlo en nuestro controlador:
public $helpers = array('Form', 'Gags');
Bien ya podemos agregar recetas ahora debemos agregar a nuestro controlador un método admin_edit de la zona admin para poder editar la receta:
public function admin_edit($recipe_id = null)
{
$this->layout = 'admin';
if (empty($this->data['Recipe'])):
$this->data = $this->Acquaintance->read(null, $recipe_id); // charge data to show in edit form
else:
Sanitize::clean($this->data['Recipe']); // antihacker line
if ($this->Recipe->save($this->data)):
$this->msgFlash(__('Data saved', true),'/admin/recipes/listing');
endif;
endif;
}
La vista admin_edit.ctp es como sigue:
<?php
echo $html->addCrumb('Control Panel', '/admin/entries/start');
echo $html->addCrumb(__('Recipes', true), '/admin/recipes/listing');
echo $html->getCrumbs(' > ');
echo $form->create('Recipe');
echo $form->hidden('Recipe.id');
?>
<fieldset>
<legend><?php __('Edit Recipe'); ?></legend>
<?php
echo $form->input('Recipe.title', array('size' => 40, 'maxlength' => 50, 'label'=>__('Title', true))) . '<br />';
echo $form->textarea('Recipe.recipe', array('cols'=>80, 'rows'=>20)).'<br />';
echo $form->error('Recipe.recipe');
echo $form->end(__('Save', true));
?>
</fieldset>
esta vista es idéntica a admin_add.ctp salvo que tiene una línea más:
echo $form->hidden('Recipe.id');
Esta linea sirve para enviar el id de la receta y dado que está colocado el id del Modelo Recipe CakePHP save que se trata de un update y no de un insert.
Bien ya podemos agregar ahora debemos poner las recetas disponibles para el público:
public function display($id=null)
{
$this->layout = 'portal';
$this->pageTitle = __('Show Recipes', true);
$conditions = null;
$fields = array('Recipe.id', 'Recipe.title');
$order = 'Recipe.title';
$limit = 30;
$this->set('data', $this->Recipe->findAll($conditions, $fields, $order, $limit));
$this->Portal->statics(); // Charge Portal components aka Sidebars
}
Estamos usanod el componente Portal de Karamelo asi que debemos agregarlo al controlador:
public $components = array('Portal');
la vista APP/views/recipes/display.ctp que le corresponde al método display():
<?php
//die(debug($data));
echo $html->div('title_section', __('Recipes', true));
if ( count($data) < 1 ):
echo $html->div(null, 'This user has not yet published recipes');
endif;
foreach ($data as $v):
echo $html->para(null, $html->link($v['Recipe']['title'], '/recipes/view/'.$v['recipe']['id']));
endforeach;
?>
Si deseamos acceder a nuestra lista de recetas desde http://nuestro-server.edu/recipes/display CakePHP no nos permitirá acceder a ella porque, recordemos, por default el compont Auth prohibe el acceso a todos los métodos. Para que un usuario (logeado o no) pueda ver la lista de las recetas debemos modificar el método beforeFilter del archivo recipes_controller.php y usar el método allow() de Auth para dar accesos de esta manera:
public function beforeFilter()
{
parent::beforeFilter();
$this->Auth->allow(array('display', 'view'));
}
Ahora si cualquier usuario puede ver la lista de recetas. Como ejercicio cree el método view() del contralador y la vista correspondiente para mostrar una receta individual.
Por último debemos agregar a nuestro controlador un método delete de la zona admin para borrar recetas:
public function admin_delete($recipe_id)
{
if ($this->Recipe->del($recipe_id)):
$this->msgFlash(__('Data removed', true),'/admin/recipes/listing');
endif;
}
Una última nota, a veces podemos usar la función array_push de PHP para agregar acciones al array de accione permitidas si es que el usuario está logeado:
public function beforeFilter()
{
$actions_allowed = array('display', 'view'); // estas acciones las puede ejecutar y ver todo mundo
if ($this-Auth->user()): // user is logged in
array_push($actions_allowed, 'otheraction1', 'nopublic'); // add restricted actions
endif;
$this->Auth->allow($actions);
parent::beforeFilter();
}
Y eso es todo si se desea agregar, modificar o extender Karamelo!

