Merge branch '0.9.x' into refactor-api

* 0.9.x:
  change DB so OpenIDPlugin manages OpenID tables
  Some bug fixes
  check the schema
  add some more methods to Schema
  Added hook for Aside container
  Revert "Outputting UTF-8 charset in document header irrespective of mimetype."
  FOAF for Groups.
  use schema tool to create a table
  statusize schema-related modules
  test script for schema code
  make table def method of schema code work
  start a module for schema management
This commit is contained in:
Zach Copley 2009-10-01 16:17:43 -07:00
commit b3bbaecf47
15 changed files with 744 additions and 17 deletions

View File

@ -87,6 +87,12 @@ StartShowContentBlock: Showing before the content container
EndShowContentBlock: Showing after the content container
- $action: the current action
StartShowAside: Showing before the Aside container
- $action: the current action
EndShowAside: Showing after the Aside container
- $action: the current action
StartNoticeSave: before inserting a notice (good place for content filters)
- $notice: notice being saved (no ID or URI)
@ -277,3 +283,5 @@ StartShowHeadElements: Right after the <head> tag
EndShowHeadElements: Right before the </head> tag; put <script>s here if you need them in <head>
- $action: the current action
CheckSchema: chance to check the schema

8
README
View File

@ -1037,6 +1037,14 @@ utf8: whether to talk to the database in UTF-8 mode. This is the default
with new installations, but older sites may want to turn it off
until they get their databases fixed up. See "UTF-8 database"
above for details.
schemacheck: when to let plugins check the database schema to add
tables or update them. Values can be 'runtime' (default)
or 'script'. 'runtime' can be costly (plugins check the
schema on every hit, adding potentially several db
queries, some quite long), but not everyone knows how to
run a script. If you can, set this to 'script' and run
scripts/checkschema.php whenever you install or upgrade a
plugin.
syslog
------

173
actions/foafgroup.php Normal file
View File

@ -0,0 +1,173 @@
<?php
/*
* StatusNet the distributed open-source microblogging tool
* Copyright (C) 2008, 2009, StatusNet, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Mail
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @author Toby Inkster <mail@tobyinkster.co.uk>
* @copyright 2009 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
class FoafGroupAction extends Action
{
function isReadOnly($args)
{
return true;
}
function prepare($args)
{
parent::prepare($args);
$nickname_arg = $this->arg('nickname');
if (empty($nickname_arg)) {
$this->clientError(_('No such group.'), 404);
return false;
}
$this->nickname = common_canonical_nickname($nickname_arg);
// Permanent redirect on non-canonical nickname
if ($nickname_arg != $this->nickname) {
common_redirect(common_local_url('foafgroup',
array('nickname' => $this->nickname)),
301);
return false;
}
$this->group = User_group::staticGet('nickname', $this->nickname);
if (!$this->group) {
$this->clientError(_('No such group.'), 404);
return false;
}
common_set_returnto($this->selfUrl());
return true;
}
function handle($args)
{
parent::handle($args);
header('Content-Type: application/rdf+xml');
$this->startXML();
$this->elementStart('rdf:RDF', array('xmlns:rdf' =>
'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
'xmlns:dcterms' =>
'http://purl.org/dc/terms/',
'xmlns:sioc' =>
'http://rdfs.org/sioc/ns#',
'xmlns:foaf' =>
'http://xmlns.com/foaf/0.1/',
'xmlns:statusnet' =>
'http://status.net/ont/',
'xmlns' => 'http://xmlns.com/foaf/0.1/'));
$this->showPpd(common_local_url('foafgroup', array('nickname' => $this->nickname)), $this->group->permalink());
$this->elementStart('Group', array('rdf:about' =>
$this->group->permalink()));
if ($this->group->fullname) {
$this->element('name', null, $this->group->fullname);
}
if ($this->group->description) {
$this->element('dcterms:description', null, $this->group->description);
}
if ($this->group->nickname) {
$this->element('dcterms:identifier', null, $this->group->nickname);
$this->element('nick', null, $this->group->nickname);
}
foreach ($this->group->getAliases() as $alias) {
$this->element('nick', null, $alias);
}
if ($this->group->homeUrl()) {
$this->element('weblog', array('rdf:resource' => $this->group->homeUrl()));
}
if ($this->group->homepage) {
$this->element('page', array('rdf:resource' => $this->group->homepage));
}
if ($this->group->homepage_logo) {
$this->element('depiction', array('rdf:resource' => $this->group->homepage_logo));
}
$members = $this->group->getMembers();
$member_details = array();
while ($members->fetch()) {
$member_uri = common_local_url('userbyid', array('id'=>$members->id));
$member_details[$member_uri] = array(
'nickname' => $members->nickname
);
$this->element('member', array('rdf:resource' => $member_uri));
}
$admins = $this->group->getAdmins();
while ($admins->fetch()) {
$admin_uri = common_local_url('userbyid', array('id'=>$admins->id));
$member_details[$admin_uri]['is_admin'] = true;
$this->element('statusnet:groupAdmin', array('rdf:resource' => $admin_uri));
}
$this->elementEnd('Group');
ksort($member_details);
foreach ($member_details as $uri => $details) {
if ($details['is_admin'])
{
$this->elementStart('Agent', array('rdf:about' => $uri));
$this->element('nick', null, $details['nickname']);
$this->elementStart('holdsAccount');
$this->elementStart('sioc:User', array('rdf:about'=>$uri.'#acct'));
$this->elementStart('sioc:has_function');
$this->elementStart('statusnet:GroupAdminRole');
$this->element('sioc:scope', array('rdf:resource' => $this->group->permalink()));
$this->elementEnd('statusnet:GroupAdminRole');
$this->elementEnd('sioc:has_function');
$this->elementEnd('sioc:User');
$this->elementEnd('holdsAccount');
$this->elementEnd('Agent');
}
else
{
$this->element('Agent', array(
'foaf:nick' => $details['nickname'],
'rdf:about' => $uri,
));
}
}
$this->elementEnd('rdf:RDF');
$this->endXML();
}
function showPpd($foaf_url, $person_uri)
{
$this->elementStart('Document', array('rdf:about' => $foaf_url));
$this->element('primaryTopic', array('rdf:resource' => $person_uri));
$this->elementEnd('Document');
}
}

View File

@ -345,7 +345,12 @@ class ShowgroupAction extends GroupDesignAction
'method' => 'timeline',
'argument' => $this->group->nickname.'.atom')),
sprintf(_('Notice feed for %s group (Atom)'),
$this->group->nickname)));
$this->group->nickname)),
new Feed(Feed::FOAF,
common_local_url('foafgroup',
array('nickname' => $this->group->nickname)),
sprintf(_('FOAF for %s group'),
$this->group->nickname)));
}
/**

View File

@ -195,18 +195,6 @@ create table nonce (
constraint primary key (consumer_key, ts, nonce)
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
/* One-to-many relationship of user to openid_url */
create table user_openid (
canonical varchar(255) primary key comment 'Canonical true URL',
display varchar(255) not null unique key comment 'URL for viewing, may be different from canonical',
user_id integer not null comment 'user owning this URL' references user (id),
created datetime not null comment 'date this record was created',
modified timestamp comment 'date this record was modified',
index user_openid_user_id_idx (user_id)
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
/* These are used by JanRain OpenID library */
create table oid_associations (

View File

@ -525,7 +525,10 @@ class Action extends HTMLOutputter // lawsuit
$this->showContentBlock();
Event::handle('EndShowContentBlock', array($this));
}
$this->showAside();
if (Event::handle('StartShowAside', array($this))) {
$this->showAside();
Event::handle('EndShowAside', array($this));
}
$this->elementEnd('div');
}

View File

@ -232,6 +232,12 @@ require_once INSTALLDIR.'/lib/serverexception.php';
Config::loadSettings();
// XXX: if plugins should check the schema at runtime, do that here.
if ($config['db']['schemacheck'] == 'runtime') {
Event::handle('CheckSchema');
}
// XXX: other formats here
define('NICKNAME_FMT', VALIDATE_NUM.VALIDATE_ALPHA_LOWER);

View File

@ -64,7 +64,8 @@ $default =
'utf8' => true,
'db_driver' => 'DB', # XXX: JanRain libs only work with DB
'quote_identifiers' => false,
'type' => 'mysql' ),
'type' => 'mysql',
'schemacheck' => 'runtime'), // 'runtime' or 'script'
'syslog' =>
array('appname' => 'statusnet', # for syslog
'priority' => 'debug', # XXX: currently ignored

View File

@ -106,7 +106,7 @@ class HTMLOutputter extends XMLOutputter
}
}
header('Content-Type: '.$type.'; charset=UTF-8');
header('Content-Type: '.$type);
$this->extraHeaders();
if (preg_match("/.*\/.*xml/", $type)) {

View File

@ -241,6 +241,10 @@ class Router
array('nickname' => '[a-zA-Z0-9]+'));
}
$m->connect('group/:nickname/foaf',
array('action' => 'foafgroup'),
array('nickname' => '[a-zA-Z0-9]+'));
$m->connect('group/:nickname/blocked',
array('action' => 'blockedfromgroup'),
array('nickname' => '[a-zA-Z0-9]+'));

445
lib/schema.php Normal file
View File

@ -0,0 +1,445 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* Database schema utilities
*
* PHP version 5
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Database
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @copyright 2009 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
if (!defined('STATUSNET')) {
exit(1);
}
/**
* Class representing the database schema
*
* A class representing the database schema. Can be used to
* manipulate the schema -- especially for plugins and upgrade
* utilities.
*
* @category Database
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
class Schema
{
static $_single = null;
protected $conn = null;
protected function __construct()
{
// XXX: there should be an easier way to do this.
$user = new User();
$this->conn = $user->getDatabaseConnection();
$user->free();
unset($user);
}
static function get()
{
if (empty(self::$_single)) {
self::$_single = new Schema();
}
return self::$_single;
}
public function getTableDef($name)
{
$res =& $this->conn->query('DESCRIBE ' . $name);
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
}
$td = new TableDef();
$td->name = $name;
$td->columns = array();
$row = array();
while ($res->fetchInto($row, DB_FETCHMODE_ASSOC)) {
$cd = new ColumnDef();
$cd->name = $row['Field'];
$packed = $row['Type'];
if (preg_match('/^(\w+)\((\d+)\)$/', $packed, $match)) {
$cd->type = $match[1];
$cd->size = $match[2];
} else {
$cd->type = $packed;
}
$cd->nullable = ($row['Null'] == 'YES') ? true : false;
$cd->key = $row['Key'];
$cd->default = $row['Default'];
$cd->extra = $row['Extra'];
$td->columns[] = $cd;
}
return $td;
}
public function getColumnDef($table, $column)
{
$td = $this->getTableDef($table);
foreach ($td->columns as $cd) {
if ($cd->name == $column) {
return $cd;
}
}
return null;
}
public function getIndexDef($table, $index)
{
return null;
}
public function createTable($name, $columns, $indices=null)
{
$uniques = array();
$primary = array();
$indices = array();
$sql = "CREATE TABLE $name (\n";
for ($i = 0; $i < count($columns); $i++) {
$cd =& $columns[$i];
if ($i > 0) {
$sql .= ",\n";
}
$sql .= $this->_columnSql($cd);
switch ($cd->key) {
case 'UNI':
$uniques[] = $cd->name;
break;
case 'PRI':
$primary[] = $cd->name;
break;
case 'MUL':
$indices[] = $cd->name;
break;
}
}
if (count($primary) > 0) { // it really should be...
$sql .= ",\nconstraint primary key (" . implode(',', $primary) . ")";
}
foreach ($uniques as $u) {
$sql .= ",\nunique index {$name}_{$u}_idx ($u)";
}
foreach ($indices as $i) {
$sql .= ",\nindex {$name}_{$i}_idx ($i)";
}
$sql .= "); ";
common_debug($sql);
$res =& $this->conn->query($sql);
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
}
return true;
}
public function dropTable($name)
{
$res =& $this->conn->query("DROP TABLE $name");
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
}
return true;
}
public function createIndex($table, $columnNames, $name = null)
{
if (!is_array($columnNames)) {
$columnNames = array($columnNames);
}
if (empty($name)) {
$name = "$table_".implode("_", $columnNames)."_idx";
}
$res =& $this->conn->query("ALTER TABLE $table ADD INDEX $name (".implode(",", $columnNames).")");
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
}
return true;
}
public function dropIndex($table, $name)
{
$res =& $this->conn->query("ALTER TABLE $table DROP INDEX $name");
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
}
return true;
}
public function addColumn($table, $columndef)
{
$sql = "ALTER TABLE $table ADD COLUMN " . $this->_columnSql($columndef);
$res =& $this->conn->query($sql);
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
}
return true;
}
public function modifyColumn($table, $columndef)
{
$sql = "ALTER TABLE $table MODIFY COLUMN " . $this->_columnSql($columndef);
$res =& $this->conn->query($sql);
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
}
return true;
}
public function dropColumn($table, $columnName)
{
$sql = "ALTER TABLE $table DROP COLUMN $columnName";
$res =& $this->conn->query($sql);
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
}
return true;
}
public function ensureTable($tableName, $columns, $indices=null)
{
// XXX: DB engine portability -> toilet
try {
$td = $this->getTableDef($tableName);
} catch (Exception $e) {
if (preg_match('/no such table/', $e->getMessage())) {
return $this->createTable($tableName, $columns, $indices);
} else {
throw $e;
}
}
$cur = $this->_names($td->columns);
$new = $this->_names($columns);
$toadd = array_diff($new, $cur);
$todrop = array_diff($cur, $new);
$same = array_intersect($new, $cur);
$tomod = array();
foreach ($same as $m) {
$curCol = $this->_byName($td->columns, $m);
$newCol = $this->_byName($columns, $m);
if (!$newCol->equals($curCol)) {
$tomod[] = $newCol->name;
}
}
if (count($toadd) + count($todrop) + count($tomod) == 0) {
// nothing to do
return true;
}
// For efficiency, we want this all in one
// query, instead of using our methods.
$phrase = array();
foreach ($toadd as $columnName) {
$cd = $this->_byName($columns, $columnName);
$phrase[] = 'ADD COLUMN ' . $this->_columnSql($cd);
}
foreach ($todrop as $columnName) {
$phrase[] = 'DROP COLUMN ' . $columnName;
}
foreach ($tomod as $columnName) {
$cd = $this->_byName($columns, $columnName);
$phrase[] = 'MODIFY COLUMN ' . $this->_columnSql($cd);
}
$sql = 'ALTER TABLE ' . $tableName . ' ' . implode(', ', $phrase);
$res =& $this->conn->query($sql);
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
}
return true;
}
function _names($cds)
{
$names = array();
foreach ($cds as $cd) {
$names[] = $cd->name;
}
return $names;
}
function _byName($cds, $name)
{
foreach ($cds as $cd) {
if ($cd->name == $name) {
return $cd;
}
}
return null;
}
function _columnSql($cd)
{
$sql = "{$cd->name} ";
if (!empty($cd->size)) {
$sql .= "{$cd->type}({$cd->size}) ";
} else {
$sql .= "{$cd->type} ";
}
if (!empty($cd->default)) {
$sql .= "default {$cd->default} ";
} else {
$sql .= ($cd->nullable) ? "null " : "not null ";
}
return $sql;
}
}
class TableDef
{
public $name;
public $columns;
}
class ColumnDef
{
public $name;
public $type;
public $size;
public $nullable;
public $key;
public $default;
public $extra;
function __construct($name=null, $type=null, $size=null,
$nullable=true, $key=null, $default=null,
$extra=null) {
$this->name = strtolower($name);
$this->type = strtolower($type);
$this->size = $size+0;
$this->nullable = $nullable;
$this->key = $key;
$this->default = $default;
$this->extra = $extra;
}
function equals($other)
{
return ($this->name == $other->name &&
$this->_typeMatch($other) &&
$this->_defaultMatch($other) &&
$this->_nullMatch($other) &&
$this->key == $other->key);
}
function _typeMatch($other)
{
switch ($this->type) {
case 'integer':
case 'int':
return ($other->type == 'integer' ||
$other->type == 'int');
break;
default:
return ($this->type == $other->type &&
$this->size == $other->size);
}
}
function _defaultMatch($other)
{
return ((is_null($this->default) && is_null($other->default)) ||
($this->default == $other->default));
}
function _nullMatch($other)
{
return ((!is_null($this->default) && !is_null($other->default) &&
$this->default == $other->default) ||
($this->nullable == $other->nullable));
}
}
class IndexDef
{
public $name;
public $table;
public $columns;
}

View File

@ -1165,7 +1165,7 @@ function common_negotiate_type($cprefs, $sprefs)
}
if ('text/html' === $besttype) {
return "text/html";
return "text/html; charset=utf-8";
}
return $besttype;
}

View File

@ -222,4 +222,19 @@ class OpenIDPlugin extends Plugin
return true;
}
function onCheckSchema() {
$schema = Schema::get();
$schema->ensureTable('user_openid',
array(new ColumnDef('canonical', 'varchar',
'255', false, 'PRI'),
new ColumnDef('display', 'varchar',
'255', false),
new ColumnDef('user_id', 'integer',
null, false, 'MUL'),
new ColumnDef('created', 'datetime',
null, false),
new ColumnDef('modified', 'timestamp')));
return true;
}
}

30
scripts/checkschema.php Normal file
View File

@ -0,0 +1,30 @@
#!/usr/bin/env php
<?php
/*
* StatusNet - a distributed open-source microblogging tool
* Copyright (C) 2008, 2009, StatusNet, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
$helptext = <<<END_OF_CHECKSCHEMA_HELP
Gives plugins a chance to update the database schema.
END_OF_CHECKSCHEMA_HELP;
require_once INSTALLDIR.'/scripts/commandline.inc';
Event::handle('CheckSchema');

41
scripts/showtable.php Normal file
View File

@ -0,0 +1,41 @@
#!/usr/bin/env php
<?php
/*
* StatusNet - a distributed open-source microblogging tool
* Copyright (C) 2008, 2009, StatusNet, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
$helptext = <<<END_OF_SHOWTABLE_HELP
showtable.php <tablename>
Shows the structure of a table
END_OF_SHOWTABLE_HELP;
require_once INSTALLDIR.'/scripts/commandline.inc';
if (count($args) != 1) {
show_help();
}
$name = $args[0];
$schema = Schema::get();
$td = $schema->getTableDef($name);
print_r($td);