jenkins-bot has submitted this change and it was merged. Change subject: initial release (v 0.1.1) ......................................................................
initial release (v 0.1.1) There are two PhpTags objects: * Storage * PageData Bug: T94872 Change-Id: Idffbdda34a3dae2c7570691f86b263ae3ea7aafb --- A PhpTagsStorage.hooks.php A PhpTagsStorage.json A PhpTagsStorage.php A i18n/en.json A i18n/qqq.json A includes/PageData.php A includes/Storage.php A includes/Storage/Field.php A includes/Storage/PageDataUpdate.php A includes/Storage/PageTemplatesUpdate.php A includes/Storage/Schema.php A includes/Storage/SchemaUpdate.php A sql/storage.sql A tests/phpunit/!!!firstInit_Test.php A tests/phpunit/StorageWikiPage_Test.php A tests/phpunit/Storage_Test.php 16 files changed, 1,128 insertions(+), 0 deletions(-) Approvals: Pastakhov: Looks good to me, approved jenkins-bot: Verified diff --git a/PhpTagsStorage.hooks.php b/PhpTagsStorage.hooks.php new file mode 100644 index 0000000..59ef574 --- /dev/null +++ b/PhpTagsStorage.hooks.php @@ -0,0 +1,93 @@ +<?php + + +/** + * PhpTags Storage MediaWiki Hooks. + * + * @file PhpTagsStorage.hooks.php + * @ingroup PhpTags + * @author Pavel Astakhov <pastak...@yandex.ru> + * @licence GNU General Public Licence 2.0 or later + */ +class PhpTagsStorageHooks { + + /** + * + * @return boolean + */ + public static function onParserFirstCallInit() { + if ( !defined( 'PHPTAGS_VERSION' ) ) { + throw new MWException( "\n\nYou need to have the PhpTags extension installed in order to use the PhpTags Storage extension." ); + } + $needVersion = '5.1.4'; + if ( version_compare( PHPTAGS_VERSION, $needVersion, '<' ) ) { + throw new MWException( "\n\nThis version of the PhpTags Storage extension requires the PhpTags extension $needVersion or above.\n You have " . PHPTAGS_VERSION . ". Please update it." ); + } + if ( PHPTAGS_HOOK_RELEASE != 8 ) { + throw new MWException( "\n\nThis version of the PhpTags Storage extension is outdated and not compatible with current version of the PhpTags extension.\n Please update it." ); + } + return true; + } + + /** + * + * @return boolean + */ + public static function onPhpTagsRuntimeFirstInit() { + \PhpTags\Hooks::addJsonFile( __DIR__ . '/PhpTagsStorage.json', PHPTAGS_STORAGE_VERSION ); + return true; + } + + /** + * + * @param \Title $title + * @param \Content $old + * @param bool $recursive + * @param \ParserOutput $parserOutput + * @param array $updates + * @return boolean + */ + public static function onSecondaryDataUpdates( $title, $old, $recursive, $parserOutput, &$updates ) { + wfDebugLog( 'PhpTags Storage', __METHOD__ ); +// echo __METHOD__ . '( ' . $title->getArticleID() . " )\n"; + \PhpTagsObjects\Storage::onDataUpdates( $title->getArticleID(), $updates ); + return true; + } + + /** + * + * @param \WikiPage $page + * @param type $content + * @param array $updates + */ + public static function onWikiPageDeletionUpdates( $page, $content, &$updates ) { + wfDebugLog( 'PhpTags Storage', __METHOD__ ); + $titleID = $page->getTitle()->getArticleID(); +// echo __METHOD__ . '( ' . $titleID . " )\n"; + PhpTagsStorage\Schema::onPageDelete( $titleID ); + PhpTagsObjects\Storage::onDataUpdates( $titleID, $updates ); + } + + public static function onLoadExtensionSchemaUpdates( DatabaseUpdater $updater ) { + $updater->addExtensionTable( 'phptags_schemas', __DIR__ . '/sql/storage.sql' ); + $updater->addExtensionTable( 'phptags_page_templates', __DIR__ . '/sql/storage.sql' ); + return true; + } + + /** + * + * @param array $files + * @return boolean + */ + public static function onUnitTestsList( &$files ) { + $testDir = __DIR__ . '/tests/phpunit'; + $files = array_merge( $files, glob( "$testDir/*Test.php" ) ); + return true; + } + + public static function onParserTestTables( &$tables ) { + $tables[] = 'phptags_schemas'; + $tables[] = 'phptags_page_templates'; + } + +} diff --git a/PhpTagsStorage.json b/PhpTagsStorage.json new file mode 100644 index 0000000..5d8fef2 --- /dev/null +++ b/PhpTagsStorage.json @@ -0,0 +1,48 @@ +{ + "objects": { + "Storage": { + "class": "Storage", + "METHODS": { + "__construct": { + "parameters": [ + { "type": "array", "name": "structure" } + ], + "return": "Storage", + "desc": "Returns new Storage object" + }, + "setValues": { + "parameters": [ + { "type": "array", "name": "values" } + ], + "desc": "Set values to Storage object" + } + } + }, + "PageData": { + "class": "PageData", + "METHODS": { + "__construct": { + "parameters": [ + { "type": "mixed", "name": "page", "default": "NULL" }, + { "type": "mixed", "name": "templates", "default": "NULL" } + ], + "return": "PageData", + "desc": "Returns new PageData object" + }, + "getValues": { + "parameters": [], + "return": "array", + "desc": "Get values stored by Storage object" + } + } + } + }, + "constants": { + "PHPTAGS_STORAGE_VERSION": { + "desc": "The current version of the PhpTags Storage extension as a string", + "type": "string", + "example": "3.1.7", + "link": "https://www.mediawiki.org/wiki/Extension:PhpTags_Storage" + } + } +} diff --git a/PhpTagsStorage.php b/PhpTagsStorage.php new file mode 100644 index 0000000..0f86668 --- /dev/null +++ b/PhpTagsStorage.php @@ -0,0 +1,54 @@ +<?php +/** + * Main entry point for the PhpTags Storage extension. + * + * @link https://www.mediawiki.org/wiki/Extension:PhpTags_Storage Documentation + * @file PhpTagsStorage.php + * @defgroup PhpTags + * @ingroup Extensions + * @author Pavel Astakhov <pastak...@yandex.ru> + * @licence GNU General Public Licence 2.0 or later + */ + +// Check to see if we are being called as an extension or directly +if ( !defined('MEDIAWIKI') ) { + die( 'This file is an extension to MediaWiki and thus not a valid entry point.' ); +} + +const PHPTAGS_STORAGE_VERSION = '0.1.1'; + +// Register this extension on Special:Version +$wgExtensionCredits['phptags'][] = array( + 'path' => __FILE__, + 'name' => 'PhpTags Storage', + 'version' => PHPTAGS_STORAGE_VERSION, + 'url' => 'https://www.mediawiki.org/wiki/Extension:PhpTags_Storage', + 'author' => '[https://www.mediawiki.org/wiki/User:Pastakhov Pavel Astakhov]', + 'descriptionmsg' => 'phptagsstorage-desc', + 'license-name' => 'GPL-2.0+', +); + +// Allow translations for this extension +$wgMessagesDirs['PhpTagsStorage'] = __DIR__ . '/i18n'; + +// Add hooks +$wgHooks['ParserFirstCallInit'][] = 'PhpTagsStorageHooks::onParserFirstCallInit'; +$wgHooks['PhpTagsRuntimeFirstInit'][] = 'PhpTagsStorageHooks::onPhpTagsRuntimeFirstInit'; +$wgHooks['LoadExtensionSchemaUpdates'][] = 'PhpTagsStorageHooks::onLoadExtensionSchemaUpdates'; +$wgHooks['SecondaryDataUpdates'][] = 'PhpTagsStorageHooks::onSecondaryDataUpdates'; +$wgHooks['WikiPageDeletionUpdates'][] = 'PhpTagsStorageHooks::onWikiPageDeletionUpdates'; +$wgHooks['UnitTestsList'][] = 'PhpTagsStorageHooks::onUnitTestsList'; +$wgHooks['ParserTestTables'][] = 'PhpTagsStorageHooks::onParserTestTables'; + +// Add parser tests +# $wgParserTestFiles[] = __DIR__ . '/tests/parser/PhpTagsStorageTests.txt'; + +// Preparing classes for autoloading +$wgAutoloadClasses['PhpTagsStorageHooks'] = __DIR__ . '/PhpTagsStorage.hooks.php'; +$wgAutoloadClasses['PhpTagsObjects\\Storage'] = __DIR__ . '/includes/Storage.php'; +$wgAutoloadClasses['PhpTagsObjects\\PageData'] = __DIR__ . '/includes/PageData.php'; +$wgAutoloadClasses['PhpTagsStorage\\Schema'] = __DIR__ . '/includes/Storage/Schema.php'; +$wgAutoloadClasses['PhpTagsStorage\\SchemaUpdate'] = __DIR__ . '/includes/Storage/SchemaUpdate.php'; +$wgAutoloadClasses['PhpTagsStorage\\PageDataUpdate'] = __DIR__ . '/includes/Storage/PageDataUpdate.php'; +$wgAutoloadClasses['PhpTagsStorage\\PageTemplatesUpdate'] = __DIR__ . '/includes/Storage/PageTemplatesUpdate.php'; +$wgAutoloadClasses['PhpTagsStorage\\Field'] = __DIR__ . '/includes/Storage/Field.php'; diff --git a/i18n/en.json b/i18n/en.json new file mode 100644 index 0000000..5bae24a --- /dev/null +++ b/i18n/en.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "pastakhov" + ] + }, + "phptagsstorage-desc": "Provides a lightweight way to store and query the data for the PhpTags extension" +} diff --git a/i18n/qqq.json b/i18n/qqq.json new file mode 100644 index 0000000..519677b --- /dev/null +++ b/i18n/qqq.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "pastakhov" + ] + }, + "phptagsstorage-desc": "{{desc|name=PhpTags_Storage|url=https://www.mediawiki.org/wiki/Extension:PhpTags_Storage}}" +} diff --git a/includes/PageData.php b/includes/PageData.php new file mode 100644 index 0000000..16f6fe8 --- /dev/null +++ b/includes/PageData.php @@ -0,0 +1,113 @@ +<?php +namespace PhpTagsObjects; + +/** + * Description of PageData + * + * @author pastakhov + */ +class PageData extends \PhpTags\GenericObject { + + public static $cache = array(); + private $pageID; + private $templates = array(); + + public function m___construct( $page = null, $templates = null ) { + wfDebugLog( 'PhpTags Storage', __METHOD__ ); + + $this->pageID = $page === null ? $this->pageID = \PhpTags\Renderer::getParser()->getTitle()->getArticleID() : self::getID( $page ); + if ( $templates === null ) { + $this->templates = null; + } elseif ( is_array( $templates ) ) { + foreach ( $templates as $t ) { + $this->templates[] = self::getID( $t ); + } + } else { + $this->templates[] = self::getID( $templates ); + } + return true; + } + + private static function getID ( $page ) { + if ( is_numeric( $page ) && $page > 0 ) { + $title = \Title::newFromID( $page ); + return (int)$page; + } elseif ( $page instanceof \PhpTags\GenericObject ) { + $value = $page->getValue(); + if ( $value instanceof \Title ) { + $title = $value; + } else { + return false; + } + } elseif ( is_string( $page) ) { + $title = \Title::newFromText( $page ); + } else { + return false; + } + if ( $title->isRedirect() ) { + $redirects = \WikiPage::factory( $title )->getContent()->getRedirectChain(); + if ( !$redirects ) { + return false; + } + $title = array_pop( $redirects ); + } + if ( $title && $title->exists() && $title->userCan('read') ) { + return $title->getArticleID(); + } + return false; + } + + public function m_getValues() { + wfDebugLog( 'PhpTags Storage', __METHOD__ ); + $pageTemplates = \PhpTagsStorage\Schema::getPageTemplates( $this->pageID ); + if ( $pageTemplates === false ) { // Page has no data + return false; + } + + $return = array(); + $templates = $this->templates; + if ( $templates ) { + $wrongTemplates = array_diff( $pageTemplates, $templates ); + foreach ( $wrongTemplates as $k => $wt ) { + $return[$wt] = false; + unset( $templates[$k] ); + } + if ( ! $templates ) { + return $return; + } + } else { + $templates = $pageTemplates; + } + + $pageID = $this->pageID; + $pageCache =& self::$cache[$pageID]; + if ( isset(self::$cache[$pageID]) ) { // Get data from cache + foreach ( $templates as $k => $t ) { + if ( isset( $pageCache[$t] ) ) { + $return[$t] = $pageCache[$t]; + unset( $templates[$k] ); + } + } + if ( ! $templates ) { + return $return; + } + } + + \PhpTagsStorage\Schema::loadSchema( $templates ); + $db = wfGetDB( DB_SLAVE, 'PhpTags' ); + foreach ( $templates as $t ) { + $fields = \PhpTagsStorage\Schema::getTemplateFields( $t ); + $res = $db->select( \PhpTagsStorage\Schema::TABLE_PREFIX . $t, '*', array('page_id'=>$pageID) ); + while ( $row = $res->fetchRow() ) { + $rowID = $row['row_id']; + foreach ( $fields as $f ) { + $pageCache[$t][$rowID][$f->getName()] = $row[$f->getDBName()]; + } + } + $res->free(); + $return[$t] = $pageCache[$t]; + } + return $return; + } + +} diff --git a/includes/Storage.php b/includes/Storage.php new file mode 100644 index 0000000..d1a37ce --- /dev/null +++ b/includes/Storage.php @@ -0,0 +1,95 @@ +<?php +namespace PhpTagsObjects; + +/** + * Description of Storage + * + * @author pastakhov + */ +class Storage extends \PhpTags\GenericObject { + + private $row_id; + private static $row_ids = array(); + private static $data = array(); + + public function m___construct( $structure ) { + wfDebugLog( 'PhpTags Storage', __METHOD__ ); + $frameTitle = \PhpTags\Renderer::getFrame()->getTitle(); +// echo __METHOD__ . '( ' . $frameTitle->getArticleID() . " )\n"; + $this->value = new \PhpTagsStorage\Schema( $frameTitle, $structure ); + return true; + } + + public function m_setValues( $values ) { + wfDebugLog( 'PhpTags Storage', __METHOD__ . print_r( $values, true ) ); + $schema = $this->value; + $templateID = $schema->getTemplateID(); + if ( $templateID <= 0 ) { + wfDebugLog( 'PhpTags Storage', __METHOD__ . ' has zerro template ID, skipping.'); + return; + } + if ( $templateID === \PhpTags\Renderer::getParser()->getTitle()->getArticleID() ) { + wfDebugLog( 'PhpTags Storage', __METHOD__ . ' template does not store data for itself, skipping.'); + return; + } + + $rowID = $this->getRowID(); + foreach ( $values as $fieldName => $val ) { + $field = $schema->getField( $fieldName ); + if ( $field === false ) { + unset( self::$data[$templateID][$rowID] ); + throw new \PhpTags\HookException( "Unknown field: $fieldName" ); + } + self::$data[$templateID][$rowID][$field->getDBName()] = $val; + } + + } + + private function getRowID() { + if ( $this->row_id === null ) { + $templateID = $this->value->getTemplateID(); + $scope = \PhpTags\Renderer::getScopeID( \PhpTags\Renderer::getFrame() ); + if ( false === isset( self::$row_ids[$templateID][$scope] ) ) { + self::$row_ids[$templateID][$scope] = isset( self::$row_ids[$templateID] ) ? count( self::$row_ids[$templateID] ) + 1 : 1; + } + $this->row_id = self::$row_ids[$templateID][$scope]; + } + return $this->row_id; + } + + /** + * + * @param int $titleID + * @param array $updates + */ + public static function onDataUpdates( $titleID, &$updates ) { + if ( $titleID <= 0 ) { + wfDebugLog( 'PhpTags Storage', __METHOD__ . ' has zerro template ID, skipping.'); + return; + } + \PhpTagsStorage\Schema::onDataUpdates( $titleID, $updates ); + + $templates = array(); + foreach ( self::$data as $templateID => $rows ) { + $templates[] = $templateID; + $updates[] = new \PhpTagsStorage\PageDataUpdate( $titleID, $templateID, $rows ); + } + + $oldTemplates = \PhpTagsStorage\Schema::getPageTemplates( $titleID ); + if ( $oldTemplates !== false ) { + $deleteOldTemplates = array_diff( $oldTemplates, $templates ); + \PhpTagsStorage\Schema::loadSchema( $deleteOldTemplates ); + foreach ( $deleteOldTemplates as $templateID ) { + if ( \PhpTagsStorage\Schema::getLoadedRow( $templateID ) === true ) { // schema doesn't exists + continue; + } + $updates[] = new \PhpTagsStorage\PageDataUpdate( $titleID, $templateID, false ); + } + } + + $updates[] = \PhpTagsStorage\Schema::createPageTemplatesUpdate( $titleID, $templates ); + self::$data = array(); + self::$row_ids = array(); + } + +} diff --git a/includes/Storage/Field.php b/includes/Storage/Field.php new file mode 100644 index 0000000..021ba5a --- /dev/null +++ b/includes/Storage/Field.php @@ -0,0 +1,69 @@ +<?php +namespace PhpTagsStorage; + +class Field { + + const PREFIX = 'field_'; + + private $name; + private $type; + private $templateID; + private $number; + + public function __construct( $templateID, $name, $type, $number ) { + $this->templateID = $templateID; + $this->name = $name; + $this->type = $type; + $this->number = $number; + } + + public static function newFromUserString( $templateID, $name, $typeString, $number ) { + switch ( strtolower( $typeString ) ) { + case 'int': + case 'integer': + $type = self::T_INTEGER; + break; + case 'text': + case 'string': + $type = self::T_TEXT; + break; + default: + throw new \PhpTags\HookException( "unknown colum type: $typeString" ); + } + return new self( $templateID, $name, $type, $number ); + } + + const T_INTEGER = 'INTEGER'; + const T_TEXT = 'TEXT'; + + public static function newFromDB( $templateID, $name, $info ) { + return new self( $templateID, $name, $info['type'], $info['number'] ); + } + + public function toCreateSQL() { + //global $wgDBtype; + $type = $this->type; + $name = self::PREFIX . $this->number; + return ", $name $type NULL DEFAULT NULL"; + } + + public function toSchema() { + return array( + 'type' => $this->type, + 'number' => $this->number, + ); + } + + public function getName() { + return $this->name; + } + + public function getNumber() { + return $this->number; + } + + public function getDBName() { + return self::PREFIX . $this->number; + } + +} \ No newline at end of file diff --git a/includes/Storage/PageDataUpdate.php b/includes/Storage/PageDataUpdate.php new file mode 100644 index 0000000..045f71f --- /dev/null +++ b/includes/Storage/PageDataUpdate.php @@ -0,0 +1,43 @@ +<?php +namespace PhpTagsStorage; + +/** + * + */ +class PageDataUpdate extends \DataUpdate { + + private $pageID; + private $templateID; + private $rows; + + function __construct( $pageID, $templateID, $rows ) { + parent::__construct(); + + $this->pageID = $pageID; + $this->templateID = $templateID; + $this->rows = $rows; + } + + public function doUpdate() { + wfDebugLog( 'PhpTags Storage', __METHOD__ ); + + $db = wfGetDB( DB_MASTER ); + wfDebugLog( 'PhpTags Storage', __METHOD__ . ' DELETE ' . $this->templateID . ' WHERE page_id=' . $this->pageID ); + $db->delete( Schema::TABLE_PREFIX . $this->templateID, array('page_id'=>$this->pageID) ); + unset( \PhpTagsObjects\PageData::$cache[$this->pageID] ); + + if ( $this->rows === false ) { + return; + } + $a = array(); + $pageID = $this->pageID; + foreach ( $this->rows as $rowID => $value ) { + $value['page_id'] = $pageID; + $value['row_id'] = $rowID; + $a[] = $value; + } + wfDebugLog( 'PhpTags Storage', __METHOD__ . ' INSERT ' . $this->templateID . ' VALUES ' . print_r( $a, true ) ); + $db->insert( Schema::TABLE_PREFIX . $this->templateID, $a ); + } + +} diff --git a/includes/Storage/PageTemplatesUpdate.php b/includes/Storage/PageTemplatesUpdate.php new file mode 100644 index 0000000..2f2a898 --- /dev/null +++ b/includes/Storage/PageTemplatesUpdate.php @@ -0,0 +1,43 @@ +<?php +namespace PhpTagsStorage; + +/** + * + */ +class PageTemplatesUpdate extends \DataUpdate { + + private $pageID; + private $templates; + private $schemaPageTemplates; + + function __construct( $pageID, $templates, &$pageTemplates ) { + parent::__construct(); + + $this->pageID = $pageID; + $this->templates = $templates; + $this->schemaPageTemplates =& $pageTemplates; + } + + public function doUpdate() { + wfDebugLog( 'PhpTags Storage', __METHOD__ ); + + $db = wfGetDB( DB_MASTER ); + $templates = $this->templates; + + if ( $templates ) { + wfDebugLog( 'PhpTags Storage', __METHOD__ . " REPLACE pageTemplates WHERE pageID is " . $this->pageID ); + $db->replace( + Schema::TABLE_PAGE_TEMPLATES, + array('page_id' => $this->pageID), + array('page_id' => $this->pageID, 'templates' => \FormatJson::encode( $templates )) + ); + $this->schemaPageTemplates[$this->pageID] = $templates; + return; + } + + wfDebugLog( 'PhpTags Storage', __METHOD__ . " DELETE pageTemplates WHERE pageID is " . $this->pageID ); + $db->delete( Schema::TABLE_PAGE_TEMPLATES, array('page_id'=>$this->pageID) ); + $this->schemaPageTemplates[$this->pageID] = null; + } + +} diff --git a/includes/Storage/Schema.php b/includes/Storage/Schema.php new file mode 100644 index 0000000..b2c8ae4 --- /dev/null +++ b/includes/Storage/Schema.php @@ -0,0 +1,196 @@ +<?php +namespace PhpTagsStorage; + +class Schema { + + const TABLE_PREFIX = 'phptags_storage_'; + const TABLE_SCHEMA = 'phptags_schemas'; + const TABLE_PAGE_TEMPLATES = 'phptags_page_templates'; + + /** + * + * @var \Title + */ + private $title; + + private static $schemaUpdates = array(); + + private $templateID = 0; + private static $loadedRows = array(); + private static $templates = array(); + private static $pageTemplates = array(); + + public function __construct( \Title $templateTitle, $newStructure = null ) { + $this->title = $templateTitle; + $templateID = $templateTitle->getArticleID(); + $this->templateID = $templateID; + + $this->preloadSchemas(); // try to query structures for all templates in the page if needed + $originFields = self::getTemplateFields( $templateID ); + $newFields = array(); + $number = 1; + foreach ( $newStructure as $fieldName => $fieldType ) { + $newFields[$fieldName] = Field::newFromUserString( $templateID, $fieldName, $fieldType, $number++ ); + } + + if ( $templateID <= 0 ) { + wfDebugLog( 'PhpTags Storage', __METHOD__ . ' has zerro template ID, skipping.'); + return; + } + + if ( $originFields === false ) { + wfDebugLog( 'PhpTags Storage', __METHOD__ . ' has new schema, will be created.'); + $fDropTable = false; + } else { + if ( $originFields == $newFields ) { + wfDebugLog( 'PhpTags Storage', __METHOD__ . ' has no changes in schema, skipping.'); + return; + } + wfDebugLog( 'PhpTags Storage', __METHOD__ . ' has changes in schema, will be updated.'); + $fDropTable = true; + } + + self::$templates[$templateID] = $newFields; + self::$schemaUpdates[$templateID] = new SchemaUpdate( $templateID, $newFields, $fDropTable, self::$loadedRows, self::$templates ); + } + + private function preloadSchemas() { + $pageID = \PhpTags\Renderer::getParser()->getTitle()->getArticleID(); + if ( $pageID <= 0 || isset( self::$pageTemplates[$pageID] ) ) { // already preloaded or id = 0 + return; + } + + $templates = self::getPageTemplates( $pageID ); + $templateID = $this->templateID; + if( $templateID <= 0 ) { + return; + } + + if ( $templates === false ) { + $templates = array( $templateID ); + } elseif( false === in_array( $templateID, $templates ) ) { + $templates[] = $templateID; + } + return self::loadSchema( $templates ); + } + + public static function getPageTemplates( $pageID ) { + if ( isset(self::$pageTemplates[$pageID]) ) { + if ( self::$pageTemplates[$pageID] === true ) { + return false; + } + return self::$pageTemplates[$pageID]; + } + + $db = wfGetDB( DB_SLAVE ); + $rowTemplates = $db->selectRow( self::TABLE_PAGE_TEMPLATES , 'templates', array('page_id'=>$pageID) ); + if ( $rowTemplates !== false ) { + $templates = \FormatJson::decode( $rowTemplates->templates, true ); + self::$pageTemplates[$pageID] = $templates; + return $templates; + } + + self::$pageTemplates[$pageID] = true; + return false; + } + + public static function loadSchema( $templates ) { + $tmp = array(); + foreach ( $templates as $id ) { + if ( isset( self::$loadedRows[$id] ) ) { + continue; + } + $tmp[] = $id; + } + if ( ! $tmp ) { + return true; + } + + $db = wfGetDB( DB_SLAVE ); + $schemaRows = $db->select( self::TABLE_SCHEMA, array('template_id','table_schema'), array('template_id'=>$tmp) ); + while ( $row = $schemaRows->fetchObject() ) { + self::$loadedRows[$row->template_id] = $row->table_schema; + } + foreach ( $tmp as $id ) { + if ( isset( self::$loadedRows[$id] ) ) { + continue; + } + self::$loadedRows[$id] = true; + } + return $schemaRows->numRows(); + } + + /** + * + * @param type $templateID + * @return Field[] + */ + public static function getTemplateFields( $templateID ) { + if ( $templateID <= 0 ) { + return false; + } + if ( !isset( self::$templates[$templateID] ) ) { + if ( !isset( self::$loadedRows[$templateID] ) ) { + self::loadSchema( array($templateID) ); + } + if ( self::$loadedRows[$templateID] === true ) { + self::$templates[$templateID] = true; + } else { + $fields = \FormatJson::decode( self::$loadedRows[$templateID], true ); + $objFields = array(); + foreach ( $fields as $fieldName => $fieldInfo ) { + $objFields[$fieldName] = Field::newFromDB( $templateID, $fieldName, $fieldInfo ); + } + self::$templates[$templateID] = $objFields; + } + } + return self::$templates[$templateID] === true ? false : self::$templates[$templateID]; + } + + public function getTemplateID() { + return $this->templateID; + } + + /** + * Returns filed by name + * @param string $fieldName + * @return Field + */ + public function getField( $fieldName ) { + return isset( self::$templates[$this->templateID][$fieldName] ) ? self::$templates[$this->templateID][$fieldName] : false; + } + + public static function getLoadedRow( $id ) { + return isset( self::$loadedRows[$id] ) ? self::$loadedRows[$id] : null; + } + + public static function createPageTemplatesUpdate( $pageID, $templates ) { + return new PageTemplatesUpdate( $pageID, $templates, self::$pageTemplates ); + } + + /** + * + * @param int $titleID + * @param array $updates + */ + public static function onDataUpdates( $titleID, &$updates ) { + if ( isset( self::$schemaUpdates[$titleID] ) ) { // need to update the schema + wfDebugLog( 'PhpTags Storage', __METHOD__ . ' adds UPDATE schema task for ID:' . $titleID ); + $updates[] = self::$schemaUpdates[$titleID]; +// echo 'UPDATE schema task ' . $titleID; + self::$schemaUpdates[$titleID] = null; + } elseif ( isset( self::$templates[$titleID] ) && self::$templates[$titleID] !== true ) { +// echo "...SKIP"; + } elseif ( isset( self::$loadedRows[$titleID] ) || self::loadSchema( array($titleID) ) > 0 ) { // need to drop the storage table + wfDebugLog( 'PhpTags Storage', __METHOD__ . ' adds DROP TABLE schema task for ID:' . $titleID ); + $updates[] = new SchemaUpdate( $titleID, false, true, self::$loadedRows, self::$templates ); // drop it +// echo 'DROP TABLE schema task ' . $titleID; + } +// echo "...\n"; + } + + public static function onPageDelete( $titleID ) { + unset( self::$templates[$titleID] ); + } + +} diff --git a/includes/Storage/SchemaUpdate.php b/includes/Storage/SchemaUpdate.php new file mode 100644 index 0000000..0d96eaf --- /dev/null +++ b/includes/Storage/SchemaUpdate.php @@ -0,0 +1,74 @@ +<?php +namespace PhpTagsStorage; + +/** + * + */ +class SchemaUpdate extends \DataUpdate { + + private $templateID; + private $fields; + private $fDropTable; + private $schemaLoadedRows; + private $schemaTemplates; + + function __construct( $templateID, $fields, $fDropTable, &$loadedRows, &$templates ) { + parent::__construct(); + + $this->templateID = $templateID; + $this->fields = $fields; + $this->fDropTable = $fDropTable; + $this->schemaLoadedRows =& $loadedRows; + $this->schemaTemplates =& $templates; + } + + public function doUpdate() { + wfDebugLog( 'PhpTags Storage', __METHOD__ ); + $templateID = $this->templateID; + $db = wfGetDB( DB_MASTER ); + + if ( $this->fDropTable ) { + try { + wfDebugLog( 'PhpTags Storage', __METHOD__ . " DROP TABLE $templateID"); + $db->dropTable( Schema::TABLE_PREFIX . $templateID ); + } catch ( Exception $e ) { + throw new MWException( "Caught exception ($e) while trying to drop PhpTags Storage table. " + . "Please make sure that your database user account has the DROP permission." ); + } + } + + if ( $this->fields === false ) { + wfDebugLog( 'PhpTags Storage', __METHOD__ . " DELETE TABLE_SCHEMA $templateID"); + $db->delete( Schema::TABLE_SCHEMA, array('template_id'=>$templateID) ); + $this->schemaLoadedRows[$templateID] = true; + $this->schemaTemplates[$templateID] = true; + return; + } + + # create table for template's data + $tableName = $db->tableName( Schema::TABLE_PREFIX . $templateID ); + $fields = array(); // for update table Schema::TABLE_SCHEMA + $createSQL = "CREATE TABLE $tableName ( page_id int NOT NULL, row_id int NOT NULL"; + foreach ( $this->fields as $f ) { + $createSQL .= $f->toCreateSQL(); + $fields[$f->getName()] = $f->toSchema(); + } + $createSQL .= ')'; + try { + wfDebugLog( 'PhpTags Storage', __METHOD__ . " CREATE TABLE $templateID"); + $db->query( $createSQL ); + $db->query( "CREATE UNIQUE INDEX page_row_id ON $tableName ( page_id, row_id )" ); + } catch ( Exception $e ) { + throw new MWException( "Caught exception ($e) while trying to create PhpTags Storage table. " + . "Please make sure that your database user account has the CREATE permission." ); + } + + # update table Schema::TABLE_SCHEMA + $schema = \FormatJson::encode( $fields ); + wfDebugLog( 'PhpTags Storage', __METHOD__ . " REPLACE TABLE_SCHEMA $templateID"); + $db->replace( Schema::TABLE_SCHEMA, array('template_id'=>$templateID), array('template_id'=>$templateID, 'table_schema'=>$schema) ); + $this->schemaTemplates[$templateID] = null; + $this->schemaLoadedRows[$templateID] = $schema; + } + +} diff --git a/sql/storage.sql b/sql/storage.sql new file mode 100644 index 0000000..289abac --- /dev/null +++ b/sql/storage.sql @@ -0,0 +1,13 @@ +CREATE TABLE /*_*/phptags_schemas ( + template_id int NOT NULL, + table_schema text NOT NULL +) /*$wgDBTableOptions*/; + +CREATE UNIQUE INDEX /*i*/template_id ON /*_*/phptags_schemas (template_id); + +CREATE TABLE /*_*/phptags_page_templates ( + page_id int NOT NULL, + templates text NOT NULL +) /*$wgDBTableOptions*/; + +CREATE UNIQUE INDEX /*i*/page_id ON /*_*/phptags_page_templates (page_id); diff --git "a/tests/phpunit/\041\041\041firstInit_Test.php" "b/tests/phpunit/\041\041\041firstInit_Test.php" new file mode 100644 index 0000000..74f988a --- /dev/null +++ "b/tests/phpunit/\041\041\041firstInit_Test.php" @@ -0,0 +1,8 @@ +<?php + +if ( PhpTags\Renderer::$needInitRuntime ) { + wfRunHooks( 'PhpTagsRuntimeFirstInit' ); + \PhpTags\Hooks::loadData(); + \PhpTags\Runtime::$loopsLimit = 1000; + PhpTags\Renderer::$needInitRuntime = false; +} diff --git a/tests/phpunit/StorageWikiPage_Test.php b/tests/phpunit/StorageWikiPage_Test.php new file mode 100644 index 0000000..5c2076f --- /dev/null +++ b/tests/phpunit/StorageWikiPage_Test.php @@ -0,0 +1,250 @@ +<?php + +class StorageWikiPageTest extends MediaWikiLangTestCase { + + protected function setUp() { + parent::setUp(); + $this->pages_to_delete = array(); + + LinkCache::singleton()->clear(); # avoid cached redirect status, etc + } + + + + protected function tearDown() { + foreach ( $this->pages_to_delete as $p ) { + /* @var $p WikiPage */ + + try { + if ( $p->exists() ) { + $p->doDeleteArticle( "testing done." ); + } + } catch ( MWException $ex ) { + // fail silently + } + } + parent::tearDown(); + } + + /** + * @param Title|string $title + * @param string|null $model + * @return WikiPage + */ + protected function newPage( $title ) { + if ( is_string( $title ) ) { + $title = Title::newFromText( $title ); + } + + $page = WikiPage::factory( $title ); + + $this->pages_to_delete[] = $page; + + return $page; + } + + /** + * @param string|Title|WikiPage $page + * @param string $text + * @param int $model + * + * @return WikiPage + */ + protected function createPage( $page, $text ) { + if ( is_string( $page ) || $page instanceof Title ) { + $page = $this->newPage( $page ); + } + + if ( $text instanceof Content ) { + $content = $text; + } else { + $content = ContentHandler::makeContent( $text, $page->getTitle() ); + } + $page->doEditContent( ContentHandler::makeContent( '', $page->getTitle() ), "create empty page" ); + $page->doEditContent( $content, "testing" ); + return $page; + } + + public function test_template_StorageTag() { + + ####### Create Template:StorageTag ####### + $text = ' +<phptag> +$s = new Storage( ["tag"=>"text"] ); +if( !isset( $argv[1] ) ) { + break; +} +$s->setValues( [ "tag"=>$argv[1] ] ); +</phptag>'; + + $titleStorageTag = Title::newFromText( 'StorageTag', NS_TEMPLATE ); + $templateStorageTagId = $this->createPage( $titleStorageTag, $text, CONTENT_MODEL_WIKITEXT )->getId(); + + # ------------------------ + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( PhpTagsStorage\Schema::TABLE_SCHEMA, '*', array('template_id' => $templateStorageTagId) ); + $n = $res->numRows(); + $res->free(); + + $this->assertEquals( 1, $n, 'TABLE_SCHEMA should contain one record' ); + + # ------------------------ + $this->assertTrue( $dbr->tableExists( PhpTagsStorage\Schema::TABLE_PREFIX . $templateStorageTagId ), 'Table for template data was not created' ); + + ####### Create Page1 (transclude one template StorageTag) ####### + $text = '{{StorageTag|It is TAG!}}'; + + $page_1 = $this->createPage( "Page1", $text, CONTENT_MODEL_WIKITEXT ); + $page_1_ID = $page_1->getId(); + + # ------------------------ + $template_table_name = PhpTagsStorage\Schema::TABLE_PREFIX . $templateStorageTagId; + $field_1_DB_Name = PhpTagsStorage\Field::PREFIX . 1; + $res = $dbr->select( $template_table_name, '*', array('page_id'=>$page_1_ID) ); + $n = $res->numRows(); + $this->assertEquals( 1, $n, 'template TABLE should contain one record for Page1' ); + + $row = $res->fetchRow(); + $this->assertEquals( $row[$field_1_DB_Name], 'It is TAG!' ); + $res->free(); + + ####### Create Page2 (transclude three templates StorageTag) ####### + $text = '{{StorageTag|one}}{{StorageTag|two}}{{StorageTag|three}}'; + + $pageID = $this->createPage( "Page2", $text, CONTENT_MODEL_WIKITEXT )->getId(); + + # ------------------------ + $res = $dbr->select( $template_table_name, '*', array('page_id'=>$pageID) ); + $n = $res->numRows(); + $this->assertEquals( 3, $n, 'template TABLE should contain three record for Page2' ); + + $row = $res->fetchRow(); + $this->assertEquals( $row[$field_1_DB_Name], 'one' ); + $row = $res->fetchRow(); + $this->assertEquals( $row[$field_1_DB_Name], 'two' ); + $row = $res->fetchRow(); + $this->assertEquals( $row[$field_1_DB_Name], 'three' ); + $res->free(); + + ####### Create Template:DumpTags ####### + $text = ' +<phptag> +$title = isset( $argv[1] ) ? new WTitle( $argv[1] ) : null; +$templateTitle = new WTitle( "StorageTag", NS_TEMPLATE ); +$pd = new PageData( $title, $templateTitle ); +$values = $pd->getValues(); +$rows = current( $values ); +if ( $rows ) { + $tags = array(); + foreach ( $rows as $tagRow ) { + $tags[] = $tagRow["tag"]; + } + echo "TAGS: ", implode( ", ", $tags ), ".\n"; +} else { + echo "There is no TAG\n"; +} +</phptag>'; + + $templateDumpTagsId = $this->createPage( "Template:DumpTags", $text, CONTENT_MODEL_WIKITEXT )->getId(); + + ####### Create Page 'Test template DumpTags' for Page2 data ####### + $text = '{{DumpTags|Page2}}'; + + $page = $this->createPage( "Test template DumpTags", $text, CONTENT_MODEL_WIKITEXT ); + + $options = new ParserOptions(); + $options->enableLimitReport( false ); + + $output = $page->getContent()-> getParserOutput( $page->getTitle(), null, $options ); + $this->assertEquals($output->getText(), "<p>TAGS: one, two, three.\n</p>", "Page 'Test template DumpTags'" ); + + ####### Move Template:StorageTag to Template:NewStorageTag and create redirect ####### +// var_dump( "-= MOVE TEMPLATE =-" ); + global $wgUser; + $titleNewStorageTag = Title::newFromText( "NewStorageTag", NS_TEMPLATE ); + $mp = new MovePage( $titleStorageTag, $titleNewStorageTag, true ); + $status = $mp->move( $wgUser, 'Test move Storage', true ); + $this->assertTrue( $status->isOK() ); + $this->assertEquals( $titleNewStorageTag->getArticleID(), $templateStorageTagId, 'ID should not change when moving the template' ); + $this->assertTrue( $dbr->tableExists( PhpTagsStorage\Schema::TABLE_PREFIX . $templateStorageTagId ), 'Template table was dropped, ID:' . $templateStorageTagId ); + + # ------------------------ + $res = $dbr->select( $template_table_name, '*', array('page_id'=>$page_1_ID) ); + $n = $res->numRows(); + $this->assertEquals( 1, $n, 'template TABLE should contain one record for Page1 after template move' ); + $row = $res->fetchRow(); + $this->assertEquals( $row[$field_1_DB_Name], 'It is TAG!', 'After template move' ); + $res->free(); + + ####### Create Page 'Test template DumpTags after move template' for Page2 data ####### + $text = '{{DumpTags|Page2}}'; + + $page = $this->createPage( "Test template DumpTags after move template", $text, CONTENT_MODEL_WIKITEXT ); + $output = $page->getContent()-> getParserOutput( $page->getTitle(), null, $options ); + $this->assertEquals($output->getText(), "<p>TAGS: one, two, three.\n</p>", "Page 'Test template DumpTags after move template'" ); + + ####### Create Page3 (transclude redirect StorageTag) ####### + $text = '{{StorageTag|I use #redirect to template NewStorageTag}}'; + $pageID = $this->createPage( "Page3", $text, CONTENT_MODEL_WIKITEXT )->getId(); + $res = $dbr->select( $template_table_name, '*', array('page_id'=>$pageID) ); + $n = $res->numRows(); + $this->assertEquals( 1, $n, 'template TABLE should contain record when redirect is transcluded (Page3)' ); + $row = $res->fetchRow(); + $this->assertEquals( $row[$field_1_DB_Name], 'I use #redirect to template NewStorageTag' ); + $res->free(); + + ####### Test template DumpTags for redirect page ####### + $wch = new WikitextContentHandler(); + $text = $wch->makeRedirectContent( Title::newFromText( "Page3" ) ); + $page = $this->createPage( "Redirect to Page3", $text, CONTENT_MODEL_WIKITEXT ); + $page->insertRedirect(); + + $text = '{{DumpTags|Redirect to Page3}}'; + $page = $this->createPage( "Test DumpTags for redirect page", $text, CONTENT_MODEL_WIKITEXT ); + + $output = $page->getContent()-> getParserOutput( $page->getTitle(), null, $options ); + $this->assertEquals($output->getText(), "<p>TAGS: I use #redirect to template NewStorageTag.\n</p>" ); + + ####### Delete Page1 ####### +// echo "Test delete Page1 $page_1_ID\n"; + $page_1->doDeleteArticle( 'test delete page' ); + $res = $dbr->select( $template_table_name, '*', array('page_id'=>$page_1_ID) ); + $n = $res->numRows(); + $this->assertEquals( 0, $n, 'template TABLE should not contain a record for Page1 after the page deletion' ); + $res->free(); + + ####### Undelete Page1 ####### + $archive = new PageArchive( $page_1->getTitle() ); + $archive->undelete( array(), 'restore Page1 after test delete' ); + $page_1 = WikiPage::factory( $page_1->getTitle() ); + $page_1_ID = $page_1->getId(); + + # ------------------------ + $template_table_name = PhpTagsStorage\Schema::TABLE_PREFIX . $templateStorageTagId; + $field_1_DB_Name = PhpTagsStorage\Field::PREFIX . 1; + $res = $dbr->select( $template_table_name, '*', array('page_id'=>$page_1_ID) ); + $n = $res->numRows(); + $this->assertEquals( 1, $n, 'template TABLE should contain one record for Page1 after undelete' ); + + $row = $res->fetchRow(); + $this->assertEquals( $row[$field_1_DB_Name], 'It is TAG!', 'after undelete Page1' ); + $res->free(); + + ####### Delete Template:NewStorageTag ####### +// var_dump( "\$titleNewStorageTag " . $titleNewStorageTag->getArticleID() ); + WikiPage::factory( $titleNewStorageTag )->doDeleteArticle( 'test delete template' ); +// var_dump( 'test delete template' ); + + # ------------------------ + $res = $dbr->select( PhpTagsStorage\Schema::TABLE_SCHEMA, '*', array('template_id' => $templateStorageTagId) ); + $n = $res->numRows(); + $res->free(); + //$this->assertEquals( 0, $n, 'TABLE_SCHEMA should not contain a record after template delete, ID:' . $templateStorageTagId ); + + # ------------------------ + $this->assertFalse( $dbr->tableExists( PhpTagsStorage\Schema::TABLE_PREFIX . $templateStorageTagId ), 'Table for template data was not deleted, ID:' . $templateStorageTagId ); + //$j = new RunJobs(); + //$j->execute(); + } + +} diff --git a/tests/phpunit/Storage_Test.php b/tests/phpunit/Storage_Test.php new file mode 100644 index 0000000..2563a69 --- /dev/null +++ b/tests/phpunit/Storage_Test.php @@ -0,0 +1,13 @@ +<?php +namespace PhpTags; + +class PhpTagsStorage_Test extends \PHPUnit_Framework_TestCase { + + public function testRun_PHPTAGS_STORAGE_VERSION_constant() { + $this->assertEquals( + Runtime::runSource('echo PHPTAGS_STORAGE_VERSION;'), + array(PHPTAGS_STORAGE_VERSION) + ); + } + +} -- To view, visit https://gerrit.wikimedia.org/r/218118 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: Idffbdda34a3dae2c7570691f86b263ae3ea7aafb Gerrit-PatchSet: 11 Gerrit-Project: mediawiki/extensions/PhpTagsStorage Gerrit-Branch: master Gerrit-Owner: Pastakhov <pastak...@yandex.ru> Gerrit-Reviewer: JoelKP <joelkpetters...@gmail.com> Gerrit-Reviewer: Pastakhov <pastak...@yandex.ru> Gerrit-Reviewer: Siebrand <siebr...@kitano.nl> Gerrit-Reviewer: Springle <sprin...@wikimedia.org> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits