Skip to content
Caution: You are browsing the legacy symfony 1.x part of this website.

Día 20: Los Plugins

Symfony version
Language
ORM

Ayer aprendímos a internacionalizar y localizar tus aplicaciones symfony. Una vez más, gracias al ICU standard y un montón de helpers, Symfony lo hace muy fácil.

Hoy, vamos a hablar de plugins: lo que son, lo que puede tener un plugin, y para que pueden ser utilizados.

Los Plugins

Un Plugin Symfony

Un plugin ofrece una manera de empaquetar y distribuir una parte de archivos del proyecto. Al igual que un proyecto, un plugin puede tener clases, helpers, configuraciones, tareas, módulos, esquemas, e incluso recursos Web (CSS, JavaScript, etc.).

Plugins Privados

El primer uso de los plugins es facilitar el intercambio de código entre aplicaciones, o incluso entre distintos proyectos. Recuerda que las aplicaciones symfony sólo comparten el modelo. Los Plugins dan una manera de compartir más componentes entre aplicaciones.

Si necesitas volver a utilizar el mismo esquema para los diferentes proyectos o los mismos módulos, colócalos en un plugin. Como un plugin es solo un directorio, puedes moverlo con bastante facilidad mediante la creación de un repositorio SVN y el uso de svn:externals, o con sólo copiar los archivos de un proyecto a otro.

Nosotros llamamos a estos "plugins privados" porque su uso está restringido a una sola empresa o a un solo desarrollador. No están a disposición del público.

tip

Puedes incluso crear un paquete de tus plugins privados, crear tu propio symfony plugin channel, e instalarlos via la tarea plugin:install.

Plugins Públicos

Los Public Plugins están disponibles para la comunidad para descargar e instalar. Durante este tutorial, tenemos que usar un par de plugins públicos: sfGuardPlugin y sfFormExtraPlugin.

Son exactamente los mismos que los plugins privados. La única diferencia es que cualquiera puede instalar para sus proyectos. Aprenderás más adelante sobre la manera de publicar y alojar uno público en el sitio web de plugins de Symfony.

Una Forma Diferente de Organización del Código

Hay una manera más de pensar en plugins y usarlos. Olvídate de la reutilización y el intercambio. Los Plugins se puede utilizar como una manera diferente para organizar el código. En lugar de organizar los archivos por capas: todos los modelos en lib/model/, las plantillas en templates/, ...; los archivos están juntos por su característica: todos los archivos job juntos (el modelo, módulos y plantillas), todos los archivos CMS juntos, y así.

Estructura de Archivos de un Plugin

Un plugin es sólo una estructura de directorios con los archivos organizados en una estructura previamente definida, según la naturaleza de los archivos. Hoy, pasaremos la mayor parte del código que hemos escrito para Jobeet en un sfJobeetPlugin. El layout básico que se utilizará es el siguiente:

sfJobeetPlugin/
  config/
    sfJobeetPluginConfiguration.class.php // Plugin initialization
    schema.yml                            // Database schema
    routing.yml                           // Routing
  lib/
    Jobeet.class.php                      // Classes
    helper/                               // Helpers
    filter/                               // Filter classes
    form/                                 // Form classes
    model/                                // Model classes
    task/                                 // Tasks
  modules/
    job/                                  // Modules
      actions/
      config/
      templates/
  web/                                    // Assets like JS, CSS, and images

El Plugin Jobeet

Inicializar un plugin es tan sencillo como crear un nuevo directorio bajo el directorio plugins/. Para Jobeet, vamos a crear un directorio sfJobeetPlugin:

$ mkdir plugins/sfJobeetPlugin

note

Todos los plugins deben terminar con Plugin. También es una buena costumbre usar el prefijo sf, aunque no es obligatorio.

El Modelo

Primero, mueve el archivo config/schema.yml a plugins/sfJobeetPlugin/config/:

$ mkdir plugins/sfJobeetPlugin/config/
$ mv config/schema.yml plugins/sfJobeetPlugin/config/schema.yml

note

Todos los comandos son para ambientes Unix. Si usas Windows, puedes arrastrar y soltar los archivos en el Explorador de Windows. Y si utilizas Subversion, o cualquier otra herramienta para la gestión de tu código, usa las herramientas incorporadas que ofrecen (como svn mv para mover archivos).

Mueve el modelo, formulario, filtros a plugins/sfJobeetPlugin/lib/:

$ mkdir plugins/sfJobeetPlugin/lib/
$ mv lib/model/ plugins/sfJobeetPlugin/lib/
$ mv lib/form/ plugins/sfJobeetPlugin/lib/
$ mv lib/filter/ plugins/sfJobeetPlugin/lib/

Si se ejecuta la tarea propel:build-model ahora, Symfony aún generará los archivos bajo lib/model/, que no es lo que queremos. La salida de Propel se puede configurar mediante la adición de una opción package. Abre el schema.yml y añade la siguiente configuración:

# plugins/sfJobeetPlugin/config/schema.yml
propel:
  _attributes:      { package: plugins.sfJobeetPlugin.lib.model }

Ahora Symfony generará sus archivos en plugins/sfJobeetPlugin/lib/model/. El generador de form y filter aprovechan también esta configuración cuando se generan archivos.

La tarea propel:build-sql genera un archivo SQL para crear tablas. Como el archivo es nombrado luego del package, remueve el actual:

$ rm data/sql/lib.model.schema.sql

Ahora, si ejecutas propel:build-all-load, Symfony generará archivos en el marco del directorio del plugin lib/model/ como se esperaba:

$ php symfony propel:build-all-load --no-confirmation

Después de ejecutar la tarea, comprueba que ningún directorio lib/model/ se ha creado. La tarea ha creado directorios lib/form/ y lib/filter/, sin embargo. Ambos son la base para todas las clases forms Propel en tu proyecto.

Como estos archivos son de alcance global para un proyecto, retiralos del plugin:

$ rm plugins/sfJobeetPlugin/lib/form/BaseFormPropel.class.php
$ rm plugins/sfJobeetPlugin/lib/filter/BaseFormFilterPropel.class.php

note

Si usas Symfony 1.2.0 o 1.2.1, el archivo base filter del form file esta en plugins/sfJobeetPlugin/lib/filter/base/.

También puedes mover el Jobeet.class.php al plugin:

$ mv lib/Jobeet.class.php plugins/sfJobeetPlugin/lib/

Como hemos pasado archivos, borrar la caché:

$ php symfony cc

tip

Si usas un acelerador PHP como APC y cosas extrañas pasan en este momento, reinicia Apache.

Ahora que todos los archivos de los modelos se han movido al plugin, ejecutar las pruebas para comprobar que todo funciona bien todavía:

$ php symfony test:all

Los controladores y las Vistas

El siguiente paso lógico es mover los módulos al plugin:

$ mv apps/frontend/modules plugins/sfJobeetPlugin/

Para evitar colisiones con el nombre del módulo, siempre es una buena costumbre poner un prefijo a los nombres de los módulos con el nombre del plugin:

$ mkdir plugins/sfJobeetPlugin/modules/
$ mv apps/frontend/modules/affiliate plugins/sfJobeetPlugin/modules/sfJobeetAffiliate
$ mv apps/frontend/modules/api plugins/sfJobeetPlugin/modules/sfJobeetApi
$ mv apps/frontend/modules/category plugins/sfJobeetPlugin/modules/sfJobeetCategory
$ mv apps/frontend/modules/job plugins/sfJobeetPlugin/modules/sfJobeetJob
$ mv apps/frontend/modules/language plugins/sfJobeetPlugin/modules/sfJobeetLanguage

Por cada módulo, también tienes que cambiar el nombre de la clase en todo los archivos actions.class.php y components.class.php (por ejemplo, la clase affiliateActions necesiat ser renombrada a sfJobeetAffiliateActions).

Las llamadas include_partial() y include_component() también deben ser modificadas en las siguientes plantillas:

  • sfJobeetAffiliate/templates/_form.php (change affiliate to sfJobeetAffiliate)
  • sfJobeetCategory/templates/showSuccess.atom.php
  • sfJobeetCategory/templates/showSuccess.php
  • sfJobeetJob/templates/indexSuccess.atom.php
  • sfJobeetJob/templates/indexSuccess.php
  • sfJobeetJob/templates/searchSuccess.php
  • sfJobeetJob/templates/showSuccess.php
  • apps/frontend/templates/layout.php

Actualiza las acciones search y delete :

// plugins/sfJobeetPlugin/modules/sfJobeetJob/actions/actions.class.php
class sfJobeetJobActions extends sfActions
{
  public function executeSearch(sfWebRequest $request)
  {
    if (!$query = $request->getParameter('query'))
    {
      return $this->forward('sfJobeetJob', 'index');
    }
 
    $this->jobs = JobeetJobPeer::getForLuceneQuery($query);
 
    if ($request->isXmlHttpRequest())
    {
      if ('*' == $query || !$this->jobs)
      {
        return $this->renderText('No results.');
      }
      else
      {
        return $this->renderPartial('sfJobeetJob/list', array('jobs' => $this->jobs));
      }
    }
  }
 
  public function executeDelete(sfWebRequest $request)
  {
    $request->checkCSRFProtection();
 
    $jobeet_job = $this->getRoute()->getObject();
    $jobeet_job->delete();
 
    $this->redirect('sfJobeetJob/index');
  }
 
  // ...
}

Eventualmente, modifica el archivo routing.yml para tomar en cuenta estos cambios:

# apps/frontend/config/routing.yml
affiliate:
  class:   sfPropelRouteCollection
  options:
    model:          JobeetAffiliate
    actions:        [new, create]
    object_actions: { wait: GET }
    prefix_path:    /:sf_culture/affiliate
    module:         sfJobeetAffiliate
  requirements:
    sf_culture: (?:fr|en)
 
api_jobs:
  url:     /api/:token/jobs.:sf_format
  class:   sfPropelRoute
  param:   { module: sfJobeetApi, action: list }
  options: { model: JobeetJob, type: list, method: getForToken }
  requirements:
    sf_format: (?:xml|json|yaml)
 
category:
  url:     /:sf_culture/category/:slug.:sf_format
  class:   sfPropelRoute
  param:   { module: sfJobeetCategory, action: show, sf_format: html }
  options: { model: JobeetCategory, type: object, method: doSelectForSlug }
  requirements:
    sf_format: (?:html|atom)
    sf_culture: (?:fr|en)
 
job_search:
  url:   /:sf_culture/search.:sf_format
  param: { module: sfJobeetJob, action: search, sf_format: html }
  requirements:
    sf_culture: (?:fr|en)
 
job:
  class:   sfPropelRouteCollection
  options:
    model:          JobeetJob
    column:         token
    object_actions: { publish: PUT, extend: PUT }
    prefix_path:    /:sf_culture/job
    module:         sfJobeetJob
  requirements:
    token: \w+
    sf_culture: (?:fr|en)
 
job_show_user:
  url:     /:sf_culture/job/:company_slug/:location_slug/:id/:position_slug
  class:   sfPropelRoute
  options:
    model: JobeetJob
    type: object
    method_for_criteria: doSelectActive
  param:   { module: sfJobeetJob, action: show }
  requirements:
    id:        \d+
    sf_method: GET
    sf_culture: (?:fr|en)
 
change_language:
  url:   /change_language
  param: { module: sfJobeetLanguage, action: changeLanguage }
 
localized_homepage:
  url:   /:sf_culture/
  param: { module: sfJobeetJob, action: index }
  requirements:
    sf_culture: (?:fr|en)
 
homepage:
  url:   /
  param: { module: sfJobeetJob, action: index }

Si tratas de navegar por la página web Jobeet ahora, tendrás excepciones diciendo que los módulos no están habilitados. Como los plugins son compartidos entre todas las aplicaciones en un proyecto, necesitas específicamente habilitar el módulo que necesitas para una aplicación determinada en su archivo de configuración settings.yml:

# apps/frontend/config/settings.yml
all:
  .settings:
    enabled_modules:
      - default
      - sfJobeetAffiliate
      - sfJobeetApi
      - sfJobeetCategory
      - sfJobeetJob
      - sfJobeetLanguage

El último paso de la migración es arreglar las pruebas funcionales donde probamos por el nombre del módulo.

sidebar

Activación del Plugin

Para que un plugin este disponible en un proyecto, debe estar habilitado en la clase ProjectConfiguration.

Este paso no es necesario con la configuración por defecto, ya que Symfony tiene un enfoque "black-list" que habilita todos los plugins excepto algunos de ellos:

// config/ProjectConfiguration.class.php
public function setup()
{
  $this->enableAllPluginsExcept(array('sfPropelPlugin', 'sfCompat10Plugin'));
}

Este enfoque es necesario para mantener la compatibilidad hacia atrás con viejas versiones Symfony pero es mejor tener un enfoque "white-list" y utilizar el método enablePlugins() en su lugar:

// config/ProjectConfiguration.class.php
public function setup()
{
  $this->enablePlugins(array('sfPropelPlugin', 'sfGuardPlugin', 'sfFormExtraPlugin', 'sfJobeetPlugin'));
}

Las Tareas

Las Tareas puede ser trasladadas al plugin con bastante facilidad:

$ mv lib/task plugins/sfJobeetPlugin/lib/

Los Archivos i18n

Un plugin puede tener Archivos XLIFF:

$ mv apps/frontend/i18n plugins/sfJobeetPlugin/

Las Rutas

Un plugin también puede contener reglas de enrutamiento:

$ mv apps/frontend/config/routing.yml plugins/sfJobeetPlugin/config/

Los Recursos

Incluso si es un poco contra-intuitivo, un plugin también puede contener Recursos web como imágenes, hojas de estilo, y JavaScripts. Como no queremos distribuir el Jobeet plugin, en realidad no tiene sentido, pero es posible mediante la creación de un directorio plugins/sfJobeetPlugin/web/.

Un recurso del plugin debe ser accesible en el directorio web/ del proyecto para ser visibles desde un navegador. La tarea plugin:publish-assets se encarga de ello creando enlaces simbólicos en plataformas Unix y copiando los archivos en plataformas Windows:

$ php symfony plugin:publish-assets

El Usuario

Moviendo los métodos de las clase myUser que tratan con los historiales es un poco más implicado. Se podría crear una clase JobeetUser y hacer que myUser herede de ella. Pero hay una forma mejor, sobre todo si varios plugins desean agregar nuevos métodos a la clase.

Los objetos del Nucléo de Symfony notifican eventos durante su ciclo de vida para que se puedan escuchar. En nuestro caso, tenemos que escuchar al evento user.method_not_found, que ocurre cuando un método indefinido se llama en el objeto sfUser.

Cuando Symfony es inicializado, todos los plugins también se inicializan si tienen una clase de configuración plugin:

// plugins/sfJobeetPlugin/config/sfJobeetPluginConfiguration.class.php
class sfJobeetPluginConfiguration extends sfPluginConfiguration
{
  public function initialize()
  {
    $this->dispatcher->connect('user.method_not_found', array('JobeetUser', 'methodNotFound'));
  }
}

Las notificaciones de los Eventos son gestionados por (sfEventDispatcher), el objeto event dispatcher. Registrar un listener es tan simple como llamar a connect(). El método connect() conecta un nombre del evento a un PHP ejecutable.

note

Un PHP callable es una variable PHP que puede ser utilizada por la función call_user_func() y devuelve true cuando pasa a la función is_callable(). Una cadena representa una función , y un array puede representar un método de objeto o un método de clase.

Con el código anterior en el lugar, el objeto myUser llamará al método estático methodNotFound() de la clase JobeetUser siempre que sea incapaz de encontrar un método. Es entonces cuando el método methodNotFound() se procesa.

Remueve todos los métodos de la clase myUser y crear la clase JobeetUser:

// apps/frontend/lib/myUser.class.php
class myUser extends sfBasicSecurityUser
{
}
 
// plugins/sfJobeetPlugin/lib/JobeetUser.class.php
class JobeetUser
{
  static public function methodNotFound(sfEvent $event)
  {
    if (method_exists('JobeetUser', $event['method']))
    {
      $event->setReturnValue(call_user_func_array(
        array('JobeetUser', $event['method']),
        array_merge(array($event->getSubject()), $event['arguments'])
      ));
 
      return true;
    }
  }
 
  static public function isFirstRequest(sfUser $user, $boolean = null)
  {
    if (is_null($boolean))
    {
      return $user->getAttribute('first_request', true);
    }
    else
    {
      $user->setAttribute('first_request', $boolean);
    }
  }
 
  static public function addJobToHistory(sfUser $user, JobeetJob $job)
  {
    $ids = $user->getAttribute('job_history', array());
 
    if (!in_array($job->getId(), $ids))
    {
      array_unshift($ids, $job->getId());
      $user->setAttribute('job_history', array_slice($ids, 0, 3));
    }
  }
 
  static public function getJobHistory(sfUser $user)
  {
    return JobeetJobPeer::retrieveByPks($user->getAttribute('job_history', array()));
  }
 
  static public function resetJobHistory(sfUser $user)
  {
    $user->getAttributeHolder()->remove('job_history');
  }
}

Cuando el dispatcher llama al método methodNotFound(), este pasa un objeto sfEvent.

Si existe el método en la clase JobeetUser , este se llama y su valor devuelto es subsecuentemente devuelto al notificador. Si no, Symfony tratará con el próximo listener registrado o lanzará una Rxcepción.

El método getSubject() regresa el notificador del evento, que en este caso es el actual objeto myUser.

Como siempre, al crear nuevas clases, no olvides borrar la caché antes de navegar o lanzar una ejecución de las pruebas:

$ php symfony cc

Arquitectura por defecto vs. Arquitectura de los Plugins

Utilizando la arquitectura de plugin te permite organizar el código de una manera diferente:

Plugin Architecture

Uso de Plugins

Al iniciar la aplicación de una nueva funcionalidad, o si tratas de resolver un problema clásico de la Web, hay probabilidad de que alguien ya ha resuelto el mismo problema y tal vez empaqueto la solución como un plugin symfony. Para buscar un plugin público symfony, ve a la sección
plugin del sitio web Symfony.

Como un plugin esta auto-contenido en un directorio, hay varias manera de instalarlo:

  • Usando la tarea plugin:install (esto solo funciona si el desarrollador ha creado un plugin package y lo ha subido al sitio web de Symfony)
  • Descarga el package/paquete y manualmente descomprimirlo bajo el directorio plugins/ (también es necesario que el desarrollador haya subido un package)
  • La creación de un svn:externals en plugins/ para el plugin (esto solo funciona si el desarrollador ha alojado su plugin en Subversion)

Las dos últimas formas son fáciles, pero le falta cierta flexibilidad. La primera te permite instalar la versión más reciente de acuerdo con la versión del proyecto Symfony, fácil de actualizar a la última versión estable, y administrar fácilmente las dependencias entre plugins.

Contribuyendo con un Plugin

Empaquetar un Plugin

Para crear un plugin package, es necesario agregar algunos archivos obligatorios a la estructura de directorios del plugin. En primer lugar, crear un archivo README en la raíz de directorios del plugin y explicar cómo instalar el plugin, lo que proporciona, y lo que no. The README file must be formatted with the Markdown format. Este archivo se utilizará en el sitio web Symfony como la principal pieza de la documentación. Puede probar la conversión de tu README a HTML utilizando el symfony plugin dingus.

sidebar

Tareas para Crear Plugins

Si te encuentras con frecuencia en la creación de plugins privados y / o públicos, considera tomar las ventajas de algunas de las tasks en el sfTaskExtraPlugin. Este plugin, mantenida por el equipo de Symfony, incluye una serie de tareas que te ayudan a agilizar el ciclo de vida del plugin:

  • generate:plugin
  • plugin:package

También necesita crear un archivo LICENSE. La elección de una licencia no es una tarea fácil, pero la sección de symfony plugin sólo lista plugins que se liberan bajo una licencia similar a la de Symfony (MIT, BSD, LGPL, y PHP). El contenido de LICENSE se mostrará bajo la pestaña licencia de la página de tu plugin.

El último paso es crear un archivo package.xml en la raíz del directorio del plugin. Este package.xml sigue el PEAR package syntax.

note

La mejor manera de aprender la sintaxis package.xml es ciertamente hacer una copia del usado por un plugin existente.

El archivo package.xml se compone de varias partes, como puedes ver en este ejemplo:

<!-- plugins/sfJobeetPlugin/package.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<package packagerversion="1.4.1" version="2.0"
   xmlns="http://pear.php.net/dtd/package-2.0"
   xmlns:tasks="http://pear.php.net/dtd/tasks-1.0"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
   http://pear.php.net/dtd/tasks-1.0.xsd http://pear.php.net/dtd/package-2.0
   http://pear.php.net/dtd/package-2.0.xsd"
>
  <name>sfJobeetPlugin</name>
  <channel>plugins.symfony-project.org</channel>
  <summary>A job board plugin.</summary>
  <description>A job board plugin.</description>
  <lead>
    <name>Fabien POTENCIER</name>
    <user>fabpot</user>
    <email>fabien.potencier@symfony-project.com</email>
    <active>yes</active>
  </lead>
  <date>2008-12-20</date>
  <version>
    <release>1.0.0</release>
    <api>1.0.0</api>
  </version>
  <stability>
    <release>stable</release>
    <api>stable</api>
  </stability>
  <license uri="http://www.symfony-project.com/license">
    MIT license
  </license>
  <notes />
 
  <contents>
    <!-- CONTENT -->
  </contents>
 
  <dependencies>
   <!-- DEPENDENCIES -->
  </dependencies>
 
  <phprelease>
</phprelease>
 
<changelog>
  <!-- CHANGELOG -->
</changelog>
</package>

El <contents> contiene los archivos que hay que poner en el package:

<contents>
  <dir name="/">
    <file role="data" name="README" />
    <file role="data" name="LICENSE" />
 
    <dir name="config">
      <file role="data" name="config.php" />
      <file role="data" name="schema.yml" />
    </dir>
 
    <!-- ... -->
  </dir>
</contents>

El <dependencies> referencias de todas las dependencias pueda tener el plugin: PHP, Symfony, y también otros plugins. Esta información es utilizada por la tarea plugin:install para instalar el plugin y su mejor versión para el proyecto y también para instalar las dependencias necesarias en caso de ser necesario.

<dependencies>
  <required>
    <php>
      <min>5.0.0</min>
    </php>
    <pearinstaller>
      <min>1.4.1</min>
    </pearinstaller>
    <package>
      <name>symfony</name>
      <channel>pear.symfony-project.com</channel>
      <min>1.2.0</min>
      <max>1.3.0</max>
      <exclude>1.3.0</exclude>
    </package>
  </required>
</dependencies>

Siempre debes declarar una dependencia a Symfony, como lo hemos hecho aquí. Declarar un mínimo y un máximo de versión permite a la tarea plugin:install saber que versión de Symfony es obligatoria ya que las versiones puede tener algo diferente en sus APIs.

Declara una dependencia con otro plugin también es posible:

<package>
  <name>sfFooPlugin</name>
  <channel>plugins.symfony-project.org</channel>
  <min>1.0.0</min>
  <max>1.2.0</max>
  <exclude>1.2.0</exclude>
</package>

El <changelog> es opcional pero nos da información útil sobre lo que ha cambiado entre versiones. Esta información está disponible bajo la pestaña "Changelog" y también en el plugin feed.

<changelog>
  <release>
    <version>
      <release>1.0.0</release>
      <api>1.0.0</api>
    </version>
    <stability>
      <release>stable</release>
      <api>stable</api>
    </stability>
    <license uri="http://www.symfony-project.com/license">
      MIT license
    </license>
    <date>2008-12-20</date>
    <license>MIT</license>
    <notes>
       * fabien: First release of the plugin
    </notes>
  </release>
</changelog>

Alojar un Plugin en el Sitio Web Symfony

Si desarrollas un Plugin útil y quiere compartirlo con la comunidad Symfony, crea una cuenta symfony si no tiene uno ya, entonces crear un nuevo plugin.

Te convertirás automáticamente en el administrador del plugin y verás una pestaña "admin" en la interfaz. En esta pestaña, usted encontrará todo lo que necesita para gestionar tu plugin y cargar tus packages.

note

El plugin FAQ contiene una gran cantidad de información útil para los desarrolladores de plugin.

Nos vemos mañana

Crear plugins, y compartirlos con la comunidad es la mejor manera de contribuir al proyecto. Es tan fácil, que el depósito está lleno de plugins útiles, divertidas, pero también de ridículos plugins.

Feedback

tip

Este capítulo ha sido traducido por Roberto Germán Puentes Díaz. Si encuentras algún error que deseas corregir o realizar algún comentario, no dudes en enviarlo por correo a puentesdiaz [arroba] gmail.com

This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.