2013-02-22 18:50:54 +00:00
|
|
|
<?php
|
2020-12-01 09:12:08 +00:00
|
|
|
|
2021-11-25 21:35:46 +00:00
|
|
|
namespace MediaWiki\Extension\TemplateData;
|
|
|
|
|
|
|
|
use CommentStoreComment;
|
|
|
|
use EditPage;
|
|
|
|
use ExtensionRegistry;
|
|
|
|
use Html;
|
2022-03-06 16:07:22 +00:00
|
|
|
use MediaWiki\Extension\EventLogging\EventLogging;
|
2020-12-01 09:12:08 +00:00
|
|
|
use MediaWiki\MediaWikiServices;
|
2021-09-16 20:07:28 +00:00
|
|
|
use MediaWiki\Revision\RenderedRevision;
|
|
|
|
use MediaWiki\Revision\RevisionRecord;
|
|
|
|
use MediaWiki\Revision\SlotRecord;
|
|
|
|
use MediaWiki\User\UserIdentity;
|
2021-11-25 21:35:46 +00:00
|
|
|
use OutputPage;
|
|
|
|
use Parser;
|
|
|
|
use ParserOutput;
|
|
|
|
use PPFrame;
|
|
|
|
use RequestContext;
|
|
|
|
use ResourceLoader;
|
|
|
|
use Status;
|
|
|
|
use Title;
|
2020-12-01 09:12:08 +00:00
|
|
|
|
2013-02-22 18:50:54 +00:00
|
|
|
/**
|
2013-09-12 09:41:07 +00:00
|
|
|
* Hooks for TemplateData extension
|
2013-02-22 18:50:54 +00:00
|
|
|
*
|
|
|
|
* @file
|
|
|
|
* @ingroup Extensions
|
|
|
|
*/
|
|
|
|
|
2021-11-25 21:35:46 +00:00
|
|
|
class Hooks {
|
2020-12-01 09:12:08 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param EditPage $editPage
|
|
|
|
* @param OutputPage $out
|
|
|
|
*/
|
|
|
|
public static function onEditPageShowEditFormFields( EditPage $editPage, OutputPage $out ) {
|
|
|
|
// TODO: Remove when not needed any more, see T267926
|
|
|
|
if ( $out->getRequest()->getBool( 'TemplateDataGeneratorUsed' ) ) {
|
|
|
|
// Recreate the dynamically created field after the user clicked "preview"
|
|
|
|
$out->addHTML( Html::hidden( 'TemplateDataGeneratorUsed', true ) );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-02-22 18:50:54 +00:00
|
|
|
/**
|
|
|
|
* Register parser hooks
|
2020-08-19 12:14:35 +00:00
|
|
|
* @param Parser $parser
|
2013-02-22 18:50:54 +00:00
|
|
|
*/
|
2020-08-19 12:14:35 +00:00
|
|
|
public static function onParserFirstCallInit( Parser $parser ) {
|
|
|
|
$parser->setHook( 'templatedata', [ __CLASS__, 'render' ] );
|
2013-02-22 18:50:54 +00:00
|
|
|
}
|
|
|
|
|
2014-10-11 05:22:21 +00:00
|
|
|
/**
|
2015-01-03 17:24:29 +00:00
|
|
|
* Conditionally register the jquery.uls.data module, in case they've already been
|
|
|
|
* registered by the UniversalLanguageSelector extension or the VisualEditor extension.
|
2014-10-11 05:22:21 +00:00
|
|
|
*
|
2017-09-24 20:19:03 +00:00
|
|
|
* @param ResourceLoader &$resourceLoader
|
2014-10-11 05:22:21 +00:00
|
|
|
*/
|
|
|
|
public static function onResourceLoaderRegisterModules( ResourceLoader &$resourceLoader ) {
|
|
|
|
$resourceModules = $resourceLoader->getConfig()->get( 'ResourceModules' );
|
2015-01-03 17:24:29 +00:00
|
|
|
$name = 'jquery.uls.data';
|
2015-03-15 23:54:16 +00:00
|
|
|
if ( !isset( $resourceModules[$name] ) && !$resourceLoader->isModuleRegistered( $name ) ) {
|
2016-05-10 00:00:14 +00:00
|
|
|
$resourceLoader->register( [
|
|
|
|
'jquery.uls.data' => [
|
2018-04-24 23:50:31 +00:00
|
|
|
'localBasePath' => dirname( __DIR__ ),
|
2014-10-11 05:22:21 +00:00
|
|
|
'remoteExtPath' => 'TemplateData',
|
2016-05-10 00:00:14 +00:00
|
|
|
'scripts' => [
|
2014-10-11 05:22:21 +00:00
|
|
|
'lib/jquery.uls/src/jquery.uls.data.js',
|
|
|
|
'lib/jquery.uls/src/jquery.uls.data.utils.js',
|
2016-05-10 00:00:14 +00:00
|
|
|
],
|
|
|
|
'targets' => [ 'desktop', 'mobile' ],
|
|
|
|
]
|
|
|
|
] );
|
2014-10-11 05:22:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-02-22 18:50:54 +00:00
|
|
|
/**
|
2021-09-16 20:07:28 +00:00
|
|
|
* @param RenderedRevision $renderedRevision
|
|
|
|
* @param UserIdentity $user
|
|
|
|
* @param CommentStoreComment $summary
|
|
|
|
* @param int $flags
|
|
|
|
* @param Status $hookStatus
|
2017-09-01 20:44:02 +00:00
|
|
|
* @return bool
|
2013-02-22 18:50:54 +00:00
|
|
|
*/
|
2021-09-16 20:07:28 +00:00
|
|
|
public static function onMultiContentSave(
|
|
|
|
RenderedRevision $renderedRevision, UserIdentity $user, CommentStoreComment $summary, $flags, Status $hookStatus
|
2013-02-22 18:50:54 +00:00
|
|
|
) {
|
2021-09-16 20:07:28 +00:00
|
|
|
$revisionRecord = $renderedRevision->getRevision();
|
|
|
|
$contentModel = $revisionRecord
|
|
|
|
->getContent( SlotRecord::MAIN )
|
|
|
|
->getModel();
|
|
|
|
|
|
|
|
if ( $contentModel !== CONTENT_MODEL_WIKITEXT ) {
|
2021-02-19 13:58:21 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2021-09-16 20:07:28 +00:00
|
|
|
// Revision hasn't been parsed yet, so parse to know if self::render got a
|
|
|
|
// valid tag (via inclusion and transclusion) and abort save if it didn't
|
2022-04-11 16:25:03 +00:00
|
|
|
$parserOutput = $renderedRevision->getRevisionParserOutput( [ 'generate-html' => false ] );
|
2020-12-01 09:12:08 +00:00
|
|
|
$templateDataStatus = self::getStatusFromParserOutput( $parserOutput );
|
2017-09-01 20:44:02 +00:00
|
|
|
if ( $templateDataStatus instanceof Status && !$templateDataStatus->isOK() ) {
|
|
|
|
// Abort edit, show error message from TemplateDataBlob::getStatus
|
2021-09-16 20:07:28 +00:00
|
|
|
$hookStatus->merge( $templateDataStatus );
|
2017-09-01 20:44:02 +00:00
|
|
|
return false;
|
2013-02-22 18:50:54 +00:00
|
|
|
}
|
2020-12-01 09:12:08 +00:00
|
|
|
|
|
|
|
// TODO: Remove when not needed any more, see T267926
|
2022-02-16 23:31:59 +00:00
|
|
|
self::logChangeEvent( $revisionRecord, $parserOutput->getPageProperty( 'templatedata' ), $user );
|
2020-12-01 09:12:08 +00:00
|
|
|
|
2013-02-22 18:50:54 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-12-01 09:12:08 +00:00
|
|
|
/**
|
2021-09-16 20:07:28 +00:00
|
|
|
* @param RevisionRecord $revisionRecord
|
2022-02-16 23:31:59 +00:00
|
|
|
* @param ?string $newPageProperty
|
2021-09-16 20:07:28 +00:00
|
|
|
* @param UserIdentity $user
|
2020-12-01 09:12:08 +00:00
|
|
|
*/
|
2022-02-16 23:31:59 +00:00
|
|
|
private static function logChangeEvent(
|
|
|
|
RevisionRecord $revisionRecord,
|
|
|
|
?string $newPageProperty,
|
|
|
|
UserIdentity $user
|
|
|
|
) {
|
2020-12-01 09:12:08 +00:00
|
|
|
if ( !ExtensionRegistry::getInstance()->isLoaded( 'EventLogging' ) ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$services = MediaWikiServices::getInstance();
|
2021-09-16 20:07:28 +00:00
|
|
|
$page = $revisionRecord->getPage();
|
|
|
|
$props = $services->getPageProps()->getProperties( $page, 'templatedata' );
|
|
|
|
|
2020-12-01 09:12:08 +00:00
|
|
|
$pageId = $page->getId();
|
|
|
|
// The JSON strings here are guaranteed to be normalized (and possibly compressed) the same
|
|
|
|
// way. No need to normalize them again for this comparison.
|
2022-02-16 23:31:59 +00:00
|
|
|
if ( $newPageProperty === ( $props[$pageId] ?? null ) ) {
|
2020-12-01 09:12:08 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$generatorUsed = RequestContext::getMain()->getRequest()->getBool( 'TemplateDataGeneratorUsed' );
|
2021-09-16 20:07:28 +00:00
|
|
|
$userEditCount = MediaWikiServices::getInstance()->getUserEditTracker()->getUserEditCount( $user );
|
2020-12-01 09:12:08 +00:00
|
|
|
// Note: We know that irrelevant changes (e.g. whitespace changes) aren't logged here
|
|
|
|
EventLogging::logEvent(
|
|
|
|
'TemplateDataEditor',
|
2021-02-24 15:52:55 +00:00
|
|
|
-1,
|
2020-12-01 09:12:08 +00:00
|
|
|
[
|
|
|
|
// Note: The "Done" button is disabled unless something changed, which means it's
|
|
|
|
// very likely (but not guaranteed) the generator was used to make the changes
|
|
|
|
'action' => $generatorUsed ? 'save-tag-edit-generator-used' : 'save-tag-edit-no-generator',
|
|
|
|
'page_id' => $pageId,
|
2021-09-16 20:07:28 +00:00
|
|
|
'page_namespace' => $page->getNamespace(),
|
|
|
|
'page_title' => $page->getDBkey(),
|
|
|
|
'rev_id' => $revisionRecord->getId() ?? 0,
|
|
|
|
'user_edit_count' => $userEditCount ?? 0,
|
2020-12-01 09:12:08 +00:00
|
|
|
'user_id' => $user->getId(),
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2013-09-21 03:46:02 +00:00
|
|
|
/**
|
|
|
|
* Parser hook registering the GUI module only in edit pages.
|
|
|
|
*
|
|
|
|
* @param EditPage $editPage
|
|
|
|
* @param OutputPage $output
|
|
|
|
*/
|
2017-09-01 20:44:02 +00:00
|
|
|
public static function onEditPage( EditPage $editPage, OutputPage $output ) {
|
2013-09-21 03:46:02 +00:00
|
|
|
global $wgTemplateDataUseGUI;
|
|
|
|
if ( $wgTemplateDataUseGUI ) {
|
2017-09-01 20:44:02 +00:00
|
|
|
if ( $output->getTitle()->inNamespace( NS_TEMPLATE ) ) {
|
2022-04-21 13:55:43 +00:00
|
|
|
$output->addModuleStyles( 'ext.templateDataGenerator.editTemplatePage.loading' );
|
|
|
|
$output->addHTML( '<div class="tdg-editscreen-placeholder"></div>' );
|
2018-11-14 14:48:56 +00:00
|
|
|
$output->addModules( 'ext.templateDataGenerator.editTemplatePage' );
|
2013-09-21 03:46:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-02-22 18:50:54 +00:00
|
|
|
/**
|
|
|
|
* Parser hook for <templatedata>.
|
|
|
|
* If there is any JSON provided, render the template documentation on the page.
|
|
|
|
*
|
2020-09-23 02:54:19 +00:00
|
|
|
* @param string|null $input The content of the tag.
|
2017-09-01 04:57:44 +00:00
|
|
|
* @param array $args The attributes of the tag.
|
|
|
|
* @param Parser $parser Parser instance available to render
|
2013-02-22 18:50:54 +00:00
|
|
|
* wikitext into html, or parser methods.
|
2017-09-01 04:57:44 +00:00
|
|
|
* @param PPFrame $frame Can be used to see what template parameters ("{{{1}}}", etc.)
|
2013-05-16 15:01:00 +00:00
|
|
|
* this hook was used with.
|
2013-02-22 18:50:54 +00:00
|
|
|
*
|
2017-09-01 04:57:44 +00:00
|
|
|
* @return string HTML to insert in the page.
|
2013-02-22 18:50:54 +00:00
|
|
|
*/
|
2018-10-04 20:59:29 +00:00
|
|
|
public static function render( $input, $args, Parser $parser, $frame ) {
|
2020-09-23 02:54:19 +00:00
|
|
|
$ti = TemplateDataBlob::newFromJSON( wfGetDB( DB_REPLICA ), $input ?? '' );
|
2013-02-22 18:50:54 +00:00
|
|
|
|
|
|
|
$status = $ti->getStatus();
|
|
|
|
if ( !$status->isOK() ) {
|
2020-10-23 17:50:06 +00:00
|
|
|
self::setStatusToParserOutput( $parser->getOutput(), $status );
|
2022-01-28 14:17:07 +00:00
|
|
|
return Html::errorBox( $status->getHTML() );
|
2013-02-22 18:50:54 +00:00
|
|
|
}
|
|
|
|
|
2018-10-04 20:59:29 +00:00
|
|
|
// Store the blob as page property for retrieval by ApiTemplateData.
|
|
|
|
// But, don't store it if we're parsing a doc sub page, because:
|
|
|
|
// - The doc subpage should not appear in ApiTemplateData as a documented
|
|
|
|
// template itself, which is confusing to users (T54448).
|
|
|
|
// - The doc subpage should not appear at Special:PagesWithProp.
|
|
|
|
// - Storing the blob twice in the database is inefficient (T52512).
|
|
|
|
$title = $parser->getTitle();
|
|
|
|
$docPage = wfMessage( 'templatedata-doc-subpage' )->inContentLanguage();
|
|
|
|
if ( !$title->isSubpage() || $title->getSubpageText() !== $docPage->plain() ) {
|
2021-10-07 23:51:01 +00:00
|
|
|
$parser->getOutput()->setPageProperty( 'templatedata', $ti->getJSONForDatabase() );
|
2018-10-04 20:59:29 +00:00
|
|
|
}
|
2013-02-22 18:50:54 +00:00
|
|
|
|
2018-07-12 22:30:53 +00:00
|
|
|
$parser->getOutput()->addModuleStyles( [
|
|
|
|
'ext.templateData',
|
|
|
|
'ext.templateData.images',
|
2019-01-20 20:27:17 +00:00
|
|
|
'jquery.tablesorter.styles',
|
2018-07-12 22:30:53 +00:00
|
|
|
] );
|
2022-01-11 22:02:39 +00:00
|
|
|
$parser->getOutput()->addModules( [ 'jquery.tablesorter' ] );
|
2020-04-08 17:30:26 +00:00
|
|
|
$parser->getOutput()->setEnableOOUI( true );
|
2013-02-22 18:50:54 +00:00
|
|
|
|
2020-04-08 20:41:03 +00:00
|
|
|
$userLang = $parser->getOptions()->getUserLangObj();
|
|
|
|
|
|
|
|
// FIXME: this hard-codes default skin, but it is needed because
|
|
|
|
// ::getHtml() will need a theme singleton to be set.
|
|
|
|
OutputPage::setupOOUI( 'bogus', $userLang->getDir() );
|
2022-02-03 08:31:50 +00:00
|
|
|
|
|
|
|
$localizer = new TemplateDataMessageLocalizer( $userLang );
|
|
|
|
$formatter = new TemplateDataHtmlFormatter( $localizer, $userLang->getCode() );
|
|
|
|
return $formatter->getHtml( $ti );
|
2013-02-22 18:50:54 +00:00
|
|
|
}
|
2019-11-23 05:18:15 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Fetch templatedata for an array of titles.
|
|
|
|
*
|
|
|
|
* @todo Document this hook
|
|
|
|
*
|
|
|
|
* The following questions are yet to be resolved.
|
|
|
|
* (a) Should we extend functionality to looking up an array of titles instead of one?
|
|
|
|
* The signature allows for an array of titles to be passed in, but the
|
2020-01-28 19:14:12 +00:00
|
|
|
* current implementation is not optimized for the multiple-title use case.
|
2019-11-23 05:18:15 +00:00
|
|
|
* (b) Should this be a lookup service instead of this faux hook?
|
|
|
|
* This will be resolved separately.
|
|
|
|
*
|
|
|
|
* @param array $tplTitles
|
2021-11-25 21:35:46 +00:00
|
|
|
* @param \stdClass[] &$tplData
|
2019-11-23 05:18:15 +00:00
|
|
|
*/
|
2020-01-28 19:14:12 +00:00
|
|
|
public static function onParserFetchTemplateData( array $tplTitles, array &$tplData ): void {
|
2019-11-23 05:18:15 +00:00
|
|
|
$tplData = [];
|
|
|
|
|
2021-09-26 12:12:19 +00:00
|
|
|
$pageProps = MediaWikiServices::getInstance()->getPageProps();
|
2022-06-24 17:46:15 +00:00
|
|
|
$wikiPageFactory = MediaWikiServices::getInstance()->getWikiPageFactory();
|
2021-09-26 12:12:19 +00:00
|
|
|
|
2019-11-23 05:18:15 +00:00
|
|
|
// This inefficient implementation is currently tuned for
|
|
|
|
// Parsoid's use case where it requests info for exactly one title.
|
|
|
|
// For a real batch use case, this code will need an overhaul.
|
|
|
|
foreach ( $tplTitles as $tplTitle ) {
|
|
|
|
$title = Title::newFromText( $tplTitle );
|
2020-01-28 19:14:12 +00:00
|
|
|
if ( !$title ) {
|
|
|
|
// Invalid title
|
2019-11-23 05:18:15 +00:00
|
|
|
$tplData[$tplTitle] = null;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( $title->isRedirect() ) {
|
2022-06-24 17:46:15 +00:00
|
|
|
$title = $wikiPageFactory->newFromTitle( $title )->getRedirectTarget();
|
2020-01-28 19:14:12 +00:00
|
|
|
if ( !$title ) {
|
|
|
|
// Invalid redirecting title
|
2019-11-23 05:18:15 +00:00
|
|
|
$tplData[$tplTitle] = null;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( !$title->exists() ) {
|
|
|
|
$tplData[$tplTitle] = (object)[ "missing" => true ];
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2020-01-28 19:14:12 +00:00
|
|
|
// FIXME: PageProps returns takes titles but returns by page id.
|
|
|
|
// This means we need to do our own look up and hope it matches.
|
|
|
|
// Spoiler, sometimes it won't. When that happens, the user won't
|
|
|
|
// get any templatedata-based interfaces for that template.
|
|
|
|
// The fallback is to not serve data for that template, which
|
|
|
|
// the clients have to support anyway, so the impact is minimal.
|
|
|
|
// It is also expected that such race conditions resolve themselves
|
|
|
|
// after a few seconds so the old "try again later" should cover this.
|
2019-11-23 05:18:15 +00:00
|
|
|
$pageId = $title->getArticleID();
|
2021-09-26 12:12:19 +00:00
|
|
|
$props = $pageProps->getProperties( $title, 'templatedata' );
|
2020-01-28 19:14:12 +00:00
|
|
|
if ( !isset( $props[$pageId] ) ) {
|
|
|
|
// No templatedata
|
2019-11-23 05:18:15 +00:00
|
|
|
$tplData[$tplTitle] = (object)[ "notemplatedata" => true ];
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$tdb = TemplateDataBlob::newFromDatabase( wfGetDB( DB_REPLICA ), $props[$pageId] );
|
|
|
|
$status = $tdb->getStatus();
|
|
|
|
if ( !$status->isOK() ) {
|
|
|
|
// Invalid data. Parsoid has no use for the error.
|
|
|
|
$tplData[$tplTitle] = (object)[ "notemplatedata" => true ];
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$tplData[$tplTitle] = $tdb->getData();
|
|
|
|
}
|
|
|
|
}
|
2020-10-23 17:50:06 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Write the status to ParserOutput object.
|
|
|
|
* @param ParserOutput $parserOutput
|
|
|
|
* @param Status $status
|
|
|
|
*/
|
|
|
|
public static function setStatusToParserOutput( ParserOutput $parserOutput, Status $status ) {
|
2020-10-23 21:25:55 +00:00
|
|
|
$parserOutput->setExtensionData( 'TemplateDataStatus',
|
|
|
|
self::jsonSerializeStatus( $status ) );
|
2020-10-23 17:50:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param ParserOutput $parserOutput
|
|
|
|
* @return Status|null
|
|
|
|
*/
|
|
|
|
public static function getStatusFromParserOutput( ParserOutput $parserOutput ) {
|
|
|
|
$status = $parserOutput->getExtensionData( 'TemplateDataStatus' );
|
|
|
|
if ( is_array( $status ) ) {
|
|
|
|
return self::newStatusFromJson( $status );
|
|
|
|
}
|
|
|
|
return $status;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param array $status contains StatusValue ok and errors fields (does not serialize value)
|
|
|
|
* @return Status
|
|
|
|
*/
|
2021-07-23 22:51:45 +00:00
|
|
|
public static function newStatusFromJson( array $status ): Status {
|
2020-10-23 17:50:06 +00:00
|
|
|
if ( $status['ok'] ) {
|
|
|
|
return Status::newGood();
|
|
|
|
} else {
|
|
|
|
$statusObj = new Status();
|
|
|
|
$errors = $status['errors'];
|
|
|
|
foreach ( $errors as $error ) {
|
|
|
|
$statusObj->fatal( $error['message'], ...$error['params'] );
|
|
|
|
}
|
|
|
|
$warnings = $status['warnings'];
|
|
|
|
foreach ( $warnings as $warning ) {
|
|
|
|
$statusObj->warning( $warning['message'], ...$warning['params'] );
|
|
|
|
}
|
|
|
|
return $statusObj;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param Status $status
|
|
|
|
* @return array contains StatusValue ok and errors fields (does not serialize value)
|
|
|
|
*/
|
2021-07-23 22:51:45 +00:00
|
|
|
public static function jsonSerializeStatus( Status $status ): array {
|
2020-10-23 17:50:06 +00:00
|
|
|
if ( $status->isOK() ) {
|
|
|
|
return [
|
|
|
|
'ok' => true
|
|
|
|
];
|
|
|
|
} else {
|
|
|
|
list( $errorsOnlyStatus, $warningsOnlyStatus ) = $status->splitByErrorType();
|
|
|
|
// note that non-scalar values are not supported in errors or warnings
|
|
|
|
return [
|
|
|
|
'ok' => false,
|
|
|
|
'errors' => $errorsOnlyStatus->getErrors(),
|
|
|
|
'warnings' => $warningsOnlyStatus->getErrors()
|
|
|
|
];
|
|
|
|
}
|
|
|
|
}
|
2013-02-22 18:50:54 +00:00
|
|
|
}
|