# - Required for non-buggy xml library for XmlTypeCheck/UploadBaseTest (T75176).
dist: trusty
-git:
- depth: 3
- quiet: true
-
# Cache NPM and Composer directories
# <https://docs.travis-ci.com/user/caching/>
cache:
<!-- This isn't a good idea; we should be using "ID" instead of "NMTOKEN" -->
<!-- However, "NMTOKEN" is strictest definition that is both compatible with existing -->
<!-- usage ([0-9]+) and with the "ID" type. -->
- <attribute name="id" type="NMTOKEN" />
+ <attribute name="id" use="optional" type="NMTOKEN" />
<attribute name="bytes" use="optional" type="nonNegativeInteger" />
</extension>
</simpleContent>
*
* @param int $reportOnly Either self::REPORT_ONLY_MODE or self::FULL_MODE
* @return string Name of http header
+ * @throws UnexpectedValueException
*/
private function getHeaderName( $reportOnly ) {
if ( $reportOnly === self::REPORT_ONLY_MODE ) {
return 'Content-Security-Policy-Report-Only';
- } elseif ( $reportOnly === self::FULL_MODE ) {
+ }
+
+ if ( $reportOnly === self::FULL_MODE ) {
return 'Content-Security-Policy';
}
throw new UnexpectedValueException( $reportOnly );
/**
* Determine what CSP policies to set for this page
*
- * @param array|bool $config Policy configuration (Either $wgCSPHeader or $wgCSPReportOnlyHeader)
+ * @param array|bool $policyConfig Policy configuration
+ * (Either $wgCSPHeader or $wgCSPReportOnlyHeader)
* @param int $mode self::REPORT_ONLY_MODE, self::FULL_MODE
* @return string Policy directives, or empty string for no policy.
*/
}
}
// Note: default on if unspecified.
- if ( ( !isset( $policyConfig['unsafeFallback'] )
- || $policyConfig['unsafeFallback'] )
+ if ( !isset( $policyConfig['unsafeFallback'] )
+ || $policyConfig['unsafeFallback']
) {
// unsafe-inline should be ignored on browsers
// that support 'nonce-foo' sources.
'rows' => 25, // @deprecated since 1.29 No longer used in core
'showhiddencats' => 0,
'shownumberswatching' => 1,
+ 'showrollbackconfirmation' => 0,
'skin' => false,
'stubthreshold' => 0,
'thumbsize' => 5,
*/
$wgMultiContentRevisionSchemaMigrationStage = SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW;
+/**
+ * The schema to use per default when generating XML dumps. This allows sites to control
+ * explicitly when to make breaking changes to their export and dump format.
+ */
+$wgXmlDumpSchemaVersion = XML_DUMP_SCHEMA_VERSION_10;
+
/**
* Actor table schema migration stage.
*
*/
$wgEnablePartialBlocks = false;
-/**
- * Enable confirmation prompt for rollback actions to prevent accidental rollbacks.
- * May be disabled to reduce number of clicks needed to perform rollbacks.
- *
- * @since 1.33
- * @var bool
- */
-$wgEnableRollbackConfirmationPrompt = true;
-
/**
* Enable stats monitoring when Block Notices are displayed in different places around core
* and extensions.
*/
$wgPriorityHints = false;
+/**
+ * Enable Element Timing.
+ *
+ * @warning EXPERIMENTAL!
+ *
+ * @since 1.34
+ * @var bool
+ */
+$wgElementTiming = false;
+
+/**
+ * Temporary option to show rollback confirmation user settings
+ * without activating the feature itself
+ * @see T217039
+ * @since 1.33
+ * @deprecated 1.33
+ * @var bool
+ */
+$wgDisableRollbackConfirmationFeature = false;
+
/**
* For really cool vim folding this needs to be at the end:
* vim: foldmarker=@{,@} foldmethod=marker
define( 'MIGRATION_WRITE_NEW', 0x20000000 | SCHEMA_COMPAT_READ_BOTH | SCHEMA_COMPAT_WRITE_NEW );
define( 'MIGRATION_NEW', 0x30000000 | SCHEMA_COMPAT_NEW );
/**@}*/
+
+/**@{
+ * XML dump schema versions, for use with XmlDumpWriter.
+ * See also the corresponding export-nnnn.xsd files in the docs directory,
+ * which are also listed at <https://www.mediawiki.org/xml/>.
+ * Note that not all old schema versions are represented here, as several
+ * were already unsupported at the time these constants were introduced.
+ */
+define( 'XML_DUMP_SCHEMA_VERSION_10', '0.10' );
+/**@}*/
$inner = $context->msg( 'brackets' )->rawParams( $inner )->escaped();
}
+ /**
+ * FIXME
+ * Remove all references to DisableRollbackConfirmationFeature
+ * after release of rollback feature. See T199534
+ */
+ if ( !MediaWikiServices::getInstance()
+ ->getMainConfig()->get( 'DisableRollbackConfirmationFeature' ) &&
+ $context->getUser()->getBoolOption( 'showrollbackconfirmation' )
+ ) {
+ $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
+ $stats->increment( 'rollbackconfirmation.event.load' );
+ $context->getOutput()->addModules( 'mediawiki.page.rollback.confirmation' );
+ }
+
return '<span class="mw-rollback-link">' . $inner . '</span>';
}
}
$title = $rev->getTitle();
+
$query = [
'action' => 'rollback',
'from' => $rev->getUserText(),
'token' => $context->getUser()->getEditToken( 'rollback' ),
];
+
$attrs = [
'data-mw' => 'interface',
'title' => $context->msg( 'tooltip-rollback' )->text(),
+ 'data-rollback-count' => (int)$editCount
];
+
$options = [ 'known', 'noclasses' ];
if ( $context->getRequest()->getBool( 'bot' ) ) {
+ //T17999
+ $query['hidediff'] = '1';
$query['bot'] = '1';
- $query['hidediff'] = '1'; // T17999
}
$disableRollbackEditCount = false;
*
* @ingroup Actions
*/
-class RollbackAction extends FormlessAction {
+class RollbackAction extends FormAction {
public function getName() {
return 'rollback';
return 'rollback';
}
- /**
- * Temporarily unused message keys due to T88044/T136375:
- * - confirm-rollback-top
- * - confirm-rollback-button
- * - rollbackfailed
- * - rollback-missingparam
- * - rollback-success-notify
- */
+ protected function usesOOUI() {
+ return true;
+ }
+
+ protected function getDescription() {
+ return '';
+ }
+
+ public function doesWrites() {
+ return true;
+ }
+
+ public function onSuccess() {
+ return false;
+ }
+
+ public function onSubmit( $data ) {
+ return false;
+ }
+
+ protected function alterForm( HTMLForm $form ) {
+ $form->setWrapperLegendMsg( 'confirm-rollback-top' );
+ $form->setSubmitTextMsg( 'confirm-rollback-button' );
+ $form->setTokenSalt( 'rollback' );
+
+ $from = $this->getRequest()->getVal( 'from' );
+ if ( $from === null ) {
+ throw new BadRequestError( 'rollbackfailed', 'rollback-missingparam' );
+ }
+ foreach ( [ 'from', 'bot', 'hidediff', 'summary', 'token' ] as $param ) {
+ $val = $this->getRequest()->getVal( $param );
+ if ( $val !== null ) {
+ $form->addHiddenField( $param, $val );
+ }
+ }
+ }
/**
+ * @throws ConfigException
* @throws ErrorPageError
+ * @throws ReadOnlyError
+ * @throws ThrottledError
*/
- public function onView() {
- // TODO: use $this->useTransactionalTimeLimit(); when POST only
- wfTransactionalTimeLimit();
+ public function show() {
+ /**
+ * FIXME
+ * Remove temporary check of DisableRollbackConfirmationFeature
+ * after release of rollback feature. See T199534
+ */
+ $config = \MediaWiki\MediaWikiServices::getInstance()->getMainConfig();
+ if ( $config->get( 'DisableRollbackConfirmationFeature' ) == true ||
+ $this->getUser()->getOption( 'showrollbackconfirmation' ) == false ||
+ $this->getRequest()->wasPosted() ) {
+ $this->handleRollbackRequest();
+ } else {
+ $this->showRollbackConfirmationForm();
+ }
+ }
+
+ public function handleRollbackRequest() {
+ $this->enableTransactionalTimelimit();
$request = $this->getRequest();
$user = $this->getUser();
] );
}
- // @TODO: remove this hack once rollback uses POST (T88044)
- $fname = __METHOD__;
- $trxLimits = $this->context->getConfig()->get( 'TrxProfilerLimits' );
- $trxProfiler = Profiler::instance()->getTransactionProfiler();
- $trxProfiler->redefineExpectations( $trxLimits['POST'], $fname );
- DeferredUpdates::addCallableUpdate( function () use ( $trxProfiler, $trxLimits, $fname ) {
- $trxProfiler->redefineExpectations( $trxLimits['PostSend-POST'], $fname );
- } );
-
$data = null;
$errors = $this->page->doRollback(
$from,
throw new ThrottledError;
}
- if ( isset( $errors[0][0] ) &&
- ( $errors[0][0] == 'alreadyrolled' || $errors[0][0] == 'cantrollback' )
- ) {
+ if ( $this->hasRollbackRelatedErrors( $errors ) ) {
$this->getOutput()->setPageTitle( $this->msg( 'rollbackfailed' ) );
$errArray = $errors[0];
$errMsg = array_shift( $errArray );
}
}
- protected function getDescription() {
- return '';
+ /**
+ * Enables transactional time limit for POST and GET requests to RollbackAction
+ * @throws ConfigException
+ */
+ private function enableTransactionalTimelimit() {
+ // If Rollbacks are made POST-only, use $this->useTransactionalTimeLimit()
+ wfTransactionalTimeLimit();
+ if ( !$this->getRequest()->wasPosted() ) {
+ /**
+ * We apply the higher POST limits on GET requests
+ * to prevent logstash.wikimedia.org from being spammed
+ */
+ $fname = __METHOD__;
+ $trxLimits = $this->context->getConfig()->get( 'TrxProfilerLimits' );
+ $trxProfiler = Profiler::instance()->getTransactionProfiler();
+ $trxProfiler->redefineExpectations( $trxLimits['POST'], $fname );
+ DeferredUpdates::addCallableUpdate( function () use ( $trxProfiler, $trxLimits, $fname
+ ) {
+ $trxProfiler->redefineExpectations( $trxLimits['PostSend-POST'], $fname );
+ } );
+ }
}
- public function doesWrites() {
- return true;
+ private function showRollbackConfirmationForm() {
+ $form = $this->getForm();
+ if ( $form->show() ) {
+ $this->onSuccess();
+ }
+ }
+
+ protected function getFormFields() {
+ return [
+ 'intro' => [
+ 'type' => 'info',
+ 'vertical-label' => true,
+ 'raw' => true,
+ 'default' => $this->msg( 'confirm-rollback-bottom' )->parse()
+ ]
+ ];
+ }
+
+ private function hasRollbackRelatedErrors( array $errors ) {
+ return isset( $errors[0][0] ) &&
+ ( $errors[0][0] == 'alreadyrolled' ||
+ $errors[0][0] == 'cantrollback'
+ );
}
}
$result = [];
if ( $params['entirewatchlist'] ) {
// Entire watchlist mode: Just update the thing and return a success indicator
- if ( is_null( $timestamp ) ) {
- $watchedItemStore->resetAllNotificationTimestampsForUser( $user );
- } else {
- $watchedItemStore->setNotificationTimestampsForUser(
- $user,
- $timestamp
- );
- }
+ $watchedItemStore->resetAllNotificationTimestampsForUser( $user, $timestamp );
$result['notificationtimestamp'] = is_null( $timestamp )
? ''
}
// Check blocks
- if ( $user->isBlocked() ) {
+ if ( $user->isBlockedFromUpload() ) {
$this->dieBlocked( $user->getBlock() );
}
"apihelp-parse-paramvalue-prop-revid": "Adds the revision ID of the parsed page.",
"apihelp-parse-paramvalue-prop-displaytitle": "Adds the title of the parsed wikitext.",
"apihelp-parse-paramvalue-prop-headitems": "Gives items to put in the <code><head></code> of the page.",
- "apihelp-parse-paramvalue-prop-headhtml": "Gives parsed <code><head></code> of the page.",
+ "apihelp-parse-paramvalue-prop-headhtml": "Gives parsed doctype, opening <code><html></code>, <code><head></code> element and opening <code><body></code> of the page.",
"apihelp-parse-paramvalue-prop-modules": "Gives the ResourceLoader modules used on the page. To load, use <code>mw.loader.using()</code>. Either <kbd>jsconfigvars</kbd> or <kbd>encodedjsconfigvars</kbd> must be requested jointly with <kbd>modules</kbd>.",
"apihelp-parse-paramvalue-prop-jsconfigvars": "Gives the JavaScript configuration variables specific to the page. To apply, use <code>mw.config.set()</code>.",
"apihelp-parse-paramvalue-prop-encodedjsconfigvars": "Gives the JavaScript configuration variables specific to the page as a JSON string.",
$collationObject = null;
Hooks::run( 'Collation::factory', [ $collationName, &$collationObject ] );
- if ( $collationObject instanceof Collation ) {
+ if ( $collationObject instanceof self ) {
return $collationObject;
}
/** @var DumpOutput */
public $sink;
+ /** @var XmlDumpWriter */
+ private $writer;
+
/**
- * Returns the export schema version.
+ * Returns the default export schema version, as defined by $wgXmlDumpSchemaVersion.
* @return string
*/
public static function schemaVersion() {
- return "0.10";
+ global $wgXmlDumpSchemaVersion;
+ return $wgXmlDumpSchemaVersion;
}
/**
function __construct( $db, $history = self::CURRENT, $text = self::TEXT ) {
$this->db = $db;
$this->history = $history;
- $this->writer = new XmlDumpWriter();
+ $this->writer = new XmlDumpWriter( $text, self::schemaVersion() );
$this->sink = new DumpOutput();
$this->text = $text;
}
+ /**
+ * @param string $schemaVersion which schema version the generated XML should comply to.
+ * One of the values from self::$supportedSchemas, using the XML_DUMP_SCHEMA_VERSION_XX
+ * constants.
+ */
+ public function setSchemaVersion( $schemaVersion ) {
+ $this->writer = new XmlDumpWriter( $this->text, $schemaVersion );
+ }
+
/**
* Set the DumpOutput or DumpFilter object which will receive
* various row objects and XML output for filtering. Filters
* @ingroup Dump
*/
class XmlDumpWriter {
+ /**
+ * @var string[] the schema versions supported for output
+ * @final
+ */
+ public static $supportedSchemas = [
+ XML_DUMP_SCHEMA_VERSION_10,
+ ];
/**
* Title of the currently processed page
) . " exceeds the maximum allowable size ($wgMaxArticleSize KB)" );
}
+ // FIXME: process schema version 11!
$revision = new WikiRevision( $this->config );
if ( isset( $revisionInfo['id'] ) ) {
use MediaWiki\MediaWikiServices;
/**
- * Job for clearing all of the "last viewed" timestamps for a user's watchlist
+ * Job for clearing all of the "last viewed" timestamps for a user's watchlist, or setting them all
+ * to the same value.
*
* Job parameters include:
* - userId: affected user ID [required]
* - casTime: UNIX timestamp of the event that triggered this job [required]
+ * - timestamp: value to set all of the "last viewed" timestamps to [optional, defaults to null]
*
* @ingroup JobQueue
* @since 1.31
static $required = [ 'userId', 'casTime' ];
$missing = implode( ', ', array_diff( $required, array_keys( $this->params ) ) );
if ( $missing != '' ) {
- throw new InvalidArgumentException( "Missing paramter(s) $missing" );
+ throw new InvalidArgumentException( "Missing parameter(s) $missing" );
}
$this->removeDuplicates = true;
$dbw = $lbFactory->getMainLB()->getConnection( DB_MASTER );
$ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
+ $timestamp = $this->params['timestamp'] ?? null;
+ if ( $timestamp === null ) {
+ $timestampCond = 'wl_notificationtimestamp IS NOT NULL';
+ } else {
+ $timestamp = $dbw->timestamp( $timestamp );
+ $timestampCond = 'wl_notificationtimestamp != ' . $dbw->addQuotes( $timestamp ) .
+ ' OR wl_notificationtimestamp IS NULL';
+ }
+ // New notifications since the reset should not be cleared
+ $casTimeCond = 'wl_notificationtimestamp < ' .
+ $dbw->addQuotes( $dbw->timestamp( $this->params['casTime'] ) ) .
+ ' OR wl_notificationtimestamp IS NULL';
- $asOfTimes = array_unique( $dbw->selectFieldValues(
- 'watchlist',
- 'wl_notificationtimestamp',
- [ 'wl_user' => $this->params['userId'], 'wl_notificationtimestamp IS NOT NULL' ],
- __METHOD__,
- [ 'ORDER BY' => 'wl_notificationtimestamp DESC' ]
- ) );
-
- foreach ( array_chunk( $asOfTimes, $rowsPerQuery ) as $asOfTimeBatch ) {
- $dbw->update(
+ $firstBatch = true;
+ do {
+ $idsToUpdate = $dbw->selectFieldValues(
'watchlist',
- [ 'wl_notificationtimestamp' => null ],
+ 'wl_id',
[
'wl_user' => $this->params['userId'],
- 'wl_notificationtimestamp' => $asOfTimeBatch,
- // New notifications since the reset should not be cleared
- 'wl_notificationtimestamp < ' .
- $dbw->addQuotes( $dbw->timestamp( $this->params['casTime'] ) )
+ $timestampCond,
+ $casTimeCond,
],
- __METHOD__
+ __METHOD__,
+ [ 'LIMIT' => $rowsPerQuery ]
);
- $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
- }
+ if ( $idsToUpdate ) {
+ $dbw->update(
+ 'watchlist',
+ [ 'wl_notificationtimestamp' => $timestamp ],
+ [
+ 'wl_id' => $idsToUpdate,
+ // For paranoia, enforce the CAS time condition here too
+ $casTimeCond
+ ],
+ __METHOD__
+ );
+ if ( !$firstBatch ) {
+ $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
+ }
+ $firstBatch = false;
+ }
+ } while ( $idsToUpdate );
}
}
# Enforce LIKE to be case sensitive, just like MySQL
$this->query( 'PRAGMA case_sensitive_like = 1' );
+ $sync = $this->sessionVars['synchronous'] ?? null;
+ if ( in_array( $sync, [ 'EXTRA', 'FULL', 'NORMAL' ], true ) ) {
+ $this->query( "PRAGMA synchronous = $sync" );
+ }
+
return $this->conn;
}
* @return string
*/
function toHtml( $options = [] ) {
- global $wgPriorityHints;
+ global $wgPriorityHints, $wgElementTiming;
if ( count( func_get_args() ) == 2 ) {
throw new MWException( __METHOD__ . ' called in the old style' );
'decoding' => 'async',
];
+ $elementTimingName = 'thumbnail';
+
if ( $wgPriorityHints
&& !self::$firstNonIconImageRendered
&& $this->width * $this->height > 100 * 100 ) {
self::$firstNonIconImageRendered = true;
$attribs['importance'] = 'high';
+ $elementTimingName = 'thumbnail-high';
+ }
+
+ if ( $wgElementTiming ) {
+ $attribs['elementtiming'] = $elementTimingName;
}
if ( !empty( $options['custom-url-link'] ) ) {
$this->skinPreferences( $user, $context, $preferences );
$this->datetimePreferences( $user, $context, $preferences );
$this->filesPreferences( $context, $preferences );
- $this->renderingPreferences( $context, $preferences );
+ $this->renderingPreferences( $user, $context, $preferences );
$this->editingPreferences( $user, $context, $preferences );
$this->rcPreferences( $user, $context, $preferences );
$this->watchlistPreferences( $user, $context, $preferences );
}
/**
+ * @param User $user
* @param MessageLocalizer $l10n
* @param array &$defaultPreferences
*/
protected function renderingPreferences(
+ User $user,
MessageLocalizer $l10n,
&$defaultPreferences
) {
'section' => 'rendering/advancedrendering',
'label-message' => 'tog-numberheadings',
];
+
+ if ( $user->isAllowed( 'rollback' ) ) {
+ $defaultPreferences['showrollbackconfirmation'] = [
+ 'type' => 'toggle',
+ 'section' => 'rendering/advancedrendering',
+ 'label-message' => 'tog-showrollbackconfirmation',
+ ];
+
+ /**
+ * FIXME
+ * Remove temporary help text and references to DisableRollbackConfirmationFeature
+ * after release of rollback feature. See T199534
+ */
+ if ( MediaWikiServices::getInstance()
+ ->getMainConfig()->get( 'DisableRollbackConfirmationFeature' ) ) {
+ $defaultPreferences['showrollbackconfirmation']
+ ['help-message'] = 'tog-showrollbackconfirmation-prerelease-warning';
+ }
+ }
}
/**
'cssclass' => 'mw-block-confirm',
];
+ // Block Id if a block already exists matching the target
+ $a['BlockId'] = [
+ 'type' => 'hidden',
+ 'default' => '',
+ ];
+
+ // Has the form been submitted
+ $a['WasPosted'] = [
+ 'type' => 'hidden',
+ 'default' => '',
+ ];
+
$this->maybeAlterFormDefaults( $a );
// Allow extensions to add more fields
$fields['Expiry']['default'] = wfTimestamp( TS_RFC2822, $block->mExpiry );
}
+ $fields['BlockId']['default'] = $block->getId();
+
$this->alreadyBlocked = true;
$this->preErrors[] = [ 'ipb-needreblock', wfEscapeWikiText( (string)$block->getTarget() ) ];
}
+ if ( $this->getRequest()->wasPosted() ) {
+ $fields['WasPosted']['default'] = true;
+ }
+
# We always need confirmation to do HideUser
if ( $this->requestedHideUser ) {
$fields['Confirm']['type'] = 'check';
}
}
}
+
+ $pageSeenKey = $this->getPageSeenTimestampsKey( $user );
+ $this->latestUpdateCache->delete( $pageSeenKey );
+ $this->stash->delete( $pageSeenKey );
}
/**
}
/**
+ * Set the "last viewed" timestamps for certain titles on a user's watchlist.
+ *
+ * If the $targets parameter is omitted or set to [], this method simply wraps
+ * resetAllNotificationTimestampsForUser(), and in that case you should instead call that method
+ * directly; support for omitting $targets is for backwards compatibility.
+ *
+ * If $targets is omitted or set to [], timestamps will be updated for every title on the user's
+ * watchlist, and this will be done through a DeferredUpdate. If $targets is a non-empty array,
+ * only the specified titles will be updated, and this will be done immediately (not deferred).
+ *
* @since 1.27
* @param User $user
- * @param string|int $timestamp
- * @param LinkTarget[] $targets
+ * @param string|int $timestamp Value to set the "last viewed" timestamp to (null to clear)
+ * @param LinkTarget[] $targets Titles to set the timestamp for; [] means the entire watchlist
* @return bool
*/
public function setNotificationTimestampsForUser( User $user, $timestamp, array $targets = [] ) {
// Only loggedin user can have a watchlist
- if ( $user->isAnon() ) {
+ if ( $user->isAnon() || $this->readOnlyMode->isReadOnly() ) {
return false;
}
- $dbw = $this->getConnectionRef( DB_MASTER );
-
- $conds = [ 'wl_user' => $user->getId() ];
- if ( $targets ) {
- $batch = new LinkBatch( $targets );
- $conds[] = $batch->constructSet( 'wl', $dbw );
+ if ( !$targets ) {
+ // Backwards compatibility
+ $this->resetAllNotificationTimestampsForUser( $user, $timestamp );
+ return true;
}
+ $rows = $this->getTitleDbKeysGroupedByNamespace( $targets );
+
+ $dbw = $this->getConnectionRef( DB_MASTER );
if ( $timestamp !== null ) {
$timestamp = $dbw->timestamp( $timestamp );
}
+ $ticket = $this->lbFactory->getEmptyTransactionTicket( __METHOD__ );
+ $affectedSinceWait = 0;
- $dbw->update(
- 'watchlist',
- [ 'wl_notificationtimestamp' => $timestamp ],
- $conds,
- __METHOD__
- );
+ // Batch update items per namespace
+ foreach ( $rows as $namespace => $namespaceTitles ) {
+ $rowBatches = array_chunk( $namespaceTitles, $this->updateRowsPerQuery );
+ foreach ( $rowBatches as $toUpdate ) {
+ $dbw->update(
+ 'watchlist',
+ [ 'wl_notificationtimestamp' => $timestamp ],
+ [
+ 'wl_user' => $user->getId(),
+ 'wl_namespace' => $namespace,
+ 'wl_title' => $toUpdate
+ ]
+ );
+ $affectedSinceWait += $dbw->affectedRows();
+ // Wait for replication every time we've touched updateRowsPerQuery rows
+ if ( $affectedSinceWait >= $this->updateRowsPerQuery ) {
+ $this->lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
+ $affectedSinceWait = 0;
+ }
+ }
+ }
$this->uncacheUser( $user );
return $timestamp;
}
- public function resetAllNotificationTimestampsForUser( User $user ) {
+ /**
+ * Schedule a DeferredUpdate that sets all of the "last viewed" timestamps for a given user
+ * to the same value.
+ * @param User $user
+ * @param string|int|null $timestamp Value to set all timestamps to, null to clear them
+ */
+ public function resetAllNotificationTimestampsForUser( User $user, $timestamp = null ) {
// Only loggedin user can have a watchlist
if ( $user->isAnon() ) {
return;
// If the page is watched by the user (or may be watched), update the timestamp
$job = new ClearWatchlistNotificationsJob(
$user->getUserPage(),
- [ 'userId' => $user->getId(), 'casTime' => time() ]
+ [ 'userId' => $user->getId(), 'timestamp' => $timestamp, 'casTime' => time() ]
);
// Try to run this post-send
}
/**
- * @param TitleValue[] $titles
+ * @param LinkTarget[] $titles
* @return array
*/
private function getTitleDbKeysGroupedByNamespace( array $titles ) {
}
/**
+ * TODO: $s is not always a string per T218883
* @param string $s
* @return string
*/
function checkTitleEncoding( $s ) {
- Assert::parameterType( 'string', $s, '$s' );
+ if ( is_array( $s ) ) {
+ throw new MWException( 'Given array to checkTitleEncoding.' );
+ }
if ( StringUtils::isUtf8( $s ) ) {
return $s;
}
"tog-useeditwarning": "Warn me when I leave an edit page with unsaved changes",
"tog-prefershttps": "Always use a secure connection while logged in",
"tog-showrollbackconfirmation": "Show a confirmation prompt when clicking on a rollback link",
+ "tog-showrollbackconfirmation-prerelease-warning": "Please note: This feature is not available yet. If you set this preference now, your choice will be remembered [https://meta.wikimedia.org/wiki/WMDE_Technical_Wishes/Rollback#Status when the feature is released].",
"underline-always": "Always",
"underline-never": "Never",
"underline-default": "Skin or browser default",
"deleting-backlinks-warning": "<strong>Warning:</strong> [[Special:WhatLinksHere/{{FULLPAGENAME}}|Other pages]] link to or transclude the page you are about to delete.",
"deleting-subpages-warning": "<strong>Warning:</strong> The page you are about to delete has [[Special:PrefixIndex/{{FULLPAGENAME}}/|{{PLURAL:$1|a subpage|$1 subpages|51=over 50 subpages}}]].",
"rollback": "Roll back edits",
+ "rollback-confirmation-confirm": "Rollback of {{PLURAL:$1|0=these edits|one edit|$1 edits}}?",
+ "rollback-confirmation-yes": "Rollback",
+ "rollback-confirmation-no": "Cancel",
"rollbacklink": "rollback",
"rollbacklinkcount": "rollback $1 {{PLURAL:$1|edit|edits}}",
"rollbacklinkcount-morethan": "rollback more than $1 {{PLURAL:$1|edit|edits}}",
"revertpage": "Reverted edits by [[Special:Contributions/$2|$2]] ([[User talk:$2|talk]]) to last revision by [[User:$1|$1]]",
"revertpage-nouser": "Reverted edits by a hidden user to last revision by {{GENDER:$1|[[User:$1|$1]]}}",
"rollback-success": "Reverted edits by {{GENDER:$3|$1}};\nchanged back to last revision by {{GENDER:$4|$2}}.",
- "rollback-success-notify": "Reverted edits by $1;\nchanged back to last revision by $2. [$3 Show changes]",
"sessionfailure-title": "Session failure",
"sessionfailure": "There seems to be a problem with your login session;\nthis action has been canceled as a precaution against session hijacking.\nPlease resubmit the form.",
"changecontentmodel" : "Change content model of a page",
"confirm-unwatch-top": "Remove this page from your watchlist?",
"confirm-rollback-button": "OK",
"confirm-rollback-top": "Revert edits to this page?",
+ "confirm-rollback-bottom": "This action will instantly rollback the selected changes to this page.",
"confirm-mcrrestore-title": "Restore a revision",
"confirm-mcrundo-title": "Undo a change",
"mcrundofailed": "Undo failed",
"tog-norollbackdiff": "Option in [[Special:Preferences]], 'Misc' tab. Only shown for users with the rollback right. By default a diff is shown below the return screen of a rollback. Checking this preference toggle will suppress that. {{Gender}}\n{{Identical|Rollback}}",
"tog-useeditwarning": "Used as label for the checkbox in [[Special:Preferences#mw-prefsection-editing|Special:Preferences]].",
"tog-prefershttps": "Toggle option used in [[Special:Preferences]] that indicates if the user wants to use a secure connection when logged in",
- "tog-showrollbackconfirmation": "Toggle option used in [[Special:Preferences]] to enable/disable rollback confirmation prompt. Should be visible only to users with rollback rights",
+ "tog-showrollbackconfirmation": "Toggle option used in [[Special:Preferences]] to enable/disable rollback confirmation prompt. Should be visible only to users with rollback rights.",
+ "tog-showrollbackconfirmation-prerelease-warning": "Notice for wikis where the option can be set before the feature is enabled.\n\nNote: This notice is temporary and will only appear before the rollback confirmation feature is released.",
"underline-always": "Used in [[Special:Preferences#mw-prefsection-rendering|Preferences]].\n\nThis option means \"always underline links\", there are also options {{msg-mw|Underline-never}} and {{msg-mw|Underline-default}}.\n\n{{Gender}}\n{{Identical|Always}}",
"underline-never": "Used in [[Special:Preferences#mw-prefsection-rendering|Preferences]].\n\nThis option means \"never underline links\", there are also options {{msg-mw|Underline-always}} and {{msg-mw|Underline-default}}.\n\n{{Gender}}\n{{Identical|Never}}",
"underline-default": "Used in [[Special:Preferences#mw-prefsection-rendering|Preferences]].\n\nThis option means \"underline links as in your user skin or your browser\", there are also options {{msg-mw|Underline-never}} and {{msg-mw|Underline-always}}.\n\n{{Gender}}\n{{Identical|Browser default}}",
"deleting-backlinks-warning": "A warning shown when a page that is being deleted has at least one link to it or is transcluded in at least one page.",
"deleting-subpages-warning": "A warning shown when a page that is being deleted has at least one subpage. $1 is the number of subpages of the page. For any number of subpages over 50, $1 will be 51.\nSee also:\n* {{msg-mw|Deleting-backlinks-warning}}",
"rollback": "{{Identical|Rollback}}",
+ "rollback-confirmation-confirm": "Question shown to user to confirm that they want to proceed with the rollback.\n\nParameters:\n* $1 - number of edits that will be rolled back.",
+ "rollback-confirmation-yes": "Button text to confirm that a rollback should be executed.",
+ "rollback-confirmation-no": "Button text to cancel a rollback instead of executing it.",
"rollbacklink": "{{Doc-actionlink}}\nThis link text appears on the recent changes page to users who have the \"rollback\" right.\nThis message has a tooltip {{msg-mw|tooltip-rollback}}\n{{Identical|Rollback}}",
"rollbacklinkcount": "{{doc-actionlink}}\nText of the rollback link showing the number of edits to be rolled back. See also {{msg-mw|rollbacklink}}.\n\nParameters:\n* $1 - the number of edits that will be rolled back. If $1 is over the value of <code>$wgShowRollbackEditCount</code> (default: 10) {{msg-mw|rollbacklinkcount-morethan}} is used.\n\nThe rollback link is displayed with a tooltip {{msg-mw|Tooltip-rollback}}",
"rollbacklinkcount-morethan": "{{doc-actionlink}}\nText of the rollback link when a greater number of edits is to be rolled back. See also {{msg-mw|rollbacklink}}.\n\nWhen the number of edits rolled back is smaller than [[mw:Special:MyLanguage/Manual:$wgShowRollbackEditCount|$wgShowRollbackEditCount]], {{msg-mw|rollbacklinkcount}} is used instead.\n\nParameters:\n* $1 - number of edits",
"revertpage": "Parameters:\n* $1 - username 1\n* $2 - username 2\n* $3 - (Optional) revision ID of the revision reverted to\n* $4 - (Optional) timestamp of the revision reverted to\n* $5 - (Optional) revision ID of the revision reverted from\n* $6 - (Optional) timestamp of the revision reverted from\nSee also:\n* {{msg-mw|Revertpage-nouser}}\n{{Identical|Revert}}",
"revertpage-nouser": "This is a confirmation message a user sees after reverting, when the username of the version is hidden with RevisionDelete.\n\nIn other cases the message {{msg-mw|Revertpage}} is used.\n\nParameters:\n* $1 - username 1, can be used for GENDER\n* $2 - (Optional) username 2\n* $3 - (Optional) revision ID of the revision reverted to\n* $4 - (Optional) timestamp of the revision reverted to\n* $5 - (Optional) revision ID of the revision reverted from\n* $6 - (Optional) timestamp of the revision reverted from",
"rollback-success": "This message shows up on screen after successful revert (generally visible only to admins). Parameters:\n* $1 - user whose changes have been reverted\n* $2 - user who produced version, which replaces reverted version\n* $3 - the first user's name, can be used for GENDER\n* $4 - the second user's name, can be used for GENDER\n{{Identical|Revert}}\n{{Identical|Rollback}}",
- "rollback-success-notify": "Notification shown after a successful revert.\n* $1 - User whose changes have been reverted\n* $2 - User that made the edit that was restored\n* $3 - Url to the diff of the rollback\nSee also:\n* {{msg-mw|showdiff}}\n{{related|rollback-success}}\n{{Format|jquerymsg}}",
"sessionfailure-title": "Used as title of the error message {{msg-mw|Sessionfailure}}.",
"sessionfailure": "Used as error message.\n\nThe title for this error message is {{msg-mw|Sessionfailure-title}}.",
"changecontentmodel": "Title of the change content model special page",
"confirm-unwatch-top": "Used as confirmation message.",
"confirm-rollback-button": "Used as Submit button text.\n{{Identical|OK}}",
"confirm-rollback-top": "Used as confirmation message.",
+ "confirm-rollback-bottom": "Used to describe the rollback action to the user.",
"confirm-mcrrestore-title": "Title for the editless restore form.",
"confirm-mcrundo-title": "Title for the editless undo form.",
"mcrundofailed": "Title of the error page when an editless undo fails.",
use MediaWiki\MediaWikiServices;
use MediaWiki\Storage\BlobAccessException;
-use MediaWiki\Storage\BlobStore;
use MediaWiki\Storage\SqlBlobStore;
use Wikimedia\Rdbms\IMaintainableDatabase;
}
/**
- * @return BlobStore
+ * @return SqlBlobStore
*/
private function getBlobStore() {
return MediaWikiServices::getInstance()->getBlobStore();
}
/**
- * @param int|string $id Content address, or text row ID.
+ * @param int|string $address Content address, or text row ID.
* @return bool|string
*/
- private function getTextSpawned( $id ) {
+ private function getTextSpawned( $address ) {
Wikimedia\suppressWarnings();
if ( !$this->spawnProc ) {
// First time?
$this->openSpawn();
}
- $text = $this->getTextSpawnedOnce( $id );
+ $text = $this->getTextSpawnedOnce( $address );
Wikimedia\restoreWarnings();
return $text;
}
/**
- * @param int|string $id Content address, or text row ID.
+ * @param int|string $address Content address, or text row ID.
* @return bool|string
*/
- private function getTextSpawnedOnce( $id ) {
- $ok = fwrite( $this->spawnWrite, "$id\n" );
+ private function getTextSpawnedOnce( $address ) {
+ if ( is_int( $address ) || intval( $address ) ) {
+ $address = SqlBlobStore::makeAddressFromTextId( (int)$address );
+ }
+
+ $ok = fwrite( $this->spawnWrite, "$address\n" );
// $this->progress( ">> $id" );
if ( !$ok ) {
return false;
return false;
}
- // check that the text id they are sending is the one we asked for
+ // check that the text address they are sending is the one we asked for
// this avoids out of sync revision text errors we have encountered in the past
$newAddress = fgets( $this->spawnRead );
if ( $newAddress === false ) {
return false;
}
if ( strpos( $newAddress, ':' ) === false ) {
- $newId = intval( $newAddress );
- if ( $newId === false ) {
- return false;
- }
- } else {
- try {
- $newAddressFields = SqlBlobStore::splitBlobAddress( $newAddress );
- $newId = $newAddressFields[ 1 ];
- } catch ( InvalidArgumentException $ex ) {
- return false;
- }
+ $newAddress = SqlBlobStore::makeAddressFromTextId( intval( $newAddress ) );
}
- if ( $id != intval( $newId ) ) {
+
+ if ( $newAddress !== $address ) {
return false;
}
protected $reportingInterval = 100;
protected $pageCount = 0;
protected $revCount = 0;
+ protected $schemaVersion = null; // use default
protected $server = null; // use default
protected $sink = null; // Output filters
protected $lastTime = 0;
'<type>[:<options>]. <types>s: latest, notalk, namespace', false, true, false, true );
$this->addOption( 'report', 'Report position and speed after every n pages processed. ' .
'Default: 100.', false, true );
+ $this->addOption( 'schema-version', 'Schema version to use for output. ' .
+ 'Default: ' . WikiExporter::schemaVersion(), false, true );
$this->addOption( 'server', 'Force reading from MySQL server', false, true );
$this->addOption( '7ziplevel', '7zip compression level for all 7zip outputs. Used for ' .
'-mx option to 7za command.', false, true );
$sink = null;
$sinks = [];
+ $this->schemaVersion = WikiExporter::schemaVersion();
+
$options = $this->orderedOptions;
foreach ( $options as $arg ) {
$opt = $arg[0];
unset( $sink );
$sink = $filter;
+ break;
+ case 'schema-version':
+ if ( !in_array( $param, XmlDumpWriter::$supportedSchemas ) ) {
+ $this->fatalError(
+ "Unsupported schema version $param. Supported versions: " .
+ implode( ', ', XmlDumpWriter::$supportedSchemas )
+ );
+ }
+ $this->schemaVersion = $param;
break;
}
}
$db = $this->backupDb();
$exporter = new WikiExporter( $db, $history, $text );
+ $exporter->setSchemaVersion( $this->schemaVersion );
$exporter->dumpUploads = $this->dumpUploads;
$exporter->dumpUploadFileContents = $this->dumpUploadFileContents;
src: https://raw.githubusercontent.com/carhartl/jquery-cookie/v1.3.1/CHANGELOG.md
integrity: sha384-SQOHhLc7PHxHDQpGE/zv9XfXKL0A7OBu8kuyVDnHVp+zSoWyRw4xUJ+LSm5ql4kS
+jquery.form:
+ type: file
+ src: https://raw.githubusercontent.com/jquery-form/form/ff80d9ddf4/jquery.form.js
+ integrity: sha384-h4G2CrcSbixzMvrrK259cNBYaL/vS1D4+KdUN9NJDzQnTU1bQ6Avluget+Id13M7
+ dest: jquery.form.js
+
+jquery.fullscreen:
+ type: file
+ src: https://raw.githubusercontent.com/theopolisme/jquery-fullscreen/v2.1.0/jquery.fullscreen.js
+ integrity: sha384-G4KPs2d99tgcsyUnJ3eeZ1r2hEKDwZfc4+/xowL/LIemq2VVwEE8HpVAWt4WYNLR
+ dest: jquery.fullscreen.js
+
+jquery.hoverIntent:
+ type: file
+ src: https://raw.githubusercontent.com/briancherne/jquery-hoverIntent/823603fdac/jquery.hoverIntent.js
+ integrity: sha384-lca0haN0hqFGGh2aYUhtAgX9dhVHfQnTADH4svDeM6gcXnL7aFGeAi1NYwipDMyS
+ dest: jquery.hoverIntent.js
+
+jquery.jStorage:
+ type: file
+ src: https://raw.githubusercontent.com/andris9/jStorage/v0.4.12/jstorage.js
+ integrity: sha384-geMeN8k803kPp6cqRL4VNfuSM1L8DcbKRk0St/KHJzxgpX9S0y9FA6HxA/JgucrJ
+ dest: jstorage.js
+
+jquery.throttle-debounce:
+ type: file
+ src: https://raw.githubusercontent.com/cowboy/jquery-throttle-debounce/v1.1/jquery.ba-throttle-debounce.js
+ integrity: sha384-ULOy4DbAghrCqRcrTJLXOY9e4gDpWh0BeEf6xMSL0VtNudXWggcb6AmrVrl4KDAP
+ dest: jquery.ba-throttle-debounce.js
+
moment:
type: tar
src: https://codeload.github.com/moment/moment/tar.gz/2.24.0
'targets' => [ 'desktop', 'mobile' ],
],
'jquery.form' => [
- 'scripts' => 'resources/lib/jquery.form.js',
+ 'scripts' => 'resources/lib/jquery.form/jquery.form.js',
],
'jquery.fullscreen' => [
- 'scripts' => 'resources/lib/jquery.fullscreen.js',
+ 'scripts' => 'resources/lib/jquery.fullscreen/jquery.fullscreen.js',
],
'jquery.getAttrs' => [
'scripts' => 'resources/src/jquery/jquery.getAttrs.js',
'targets' => [ 'desktop', 'mobile' ],
],
'jquery.hoverIntent' => [
- 'scripts' => 'resources/lib/jquery.hoverIntent.js',
+ 'scripts' => 'resources/lib/jquery.hoverIntent/jquery.hoverIntent.js',
],
'jquery.i18n' => [
'scripts' => [
],
'jquery.jStorage' => [
'deprecated' => 'Please use "mediawiki.storage" instead.',
- 'scripts' => 'resources/lib/jquery.jStorage.js',
+ 'scripts' => 'resources/lib/jquery.jStorage/jstorage.js',
],
'jquery.suggestions' => [
'targets' => [ 'desktop', 'mobile' ],
'jquery.throttle-debounce' => [
'deprecated' => 'Please use OO.ui.throttle/debounce instead. See '
. 'https://phabricator.wikimedia.org/T213426',
- 'scripts' => 'resources/lib/jquery.ba-throttle-debounce.js',
+ 'scripts' => 'resources/lib/jquery.throttle-debounce/jquery.ba-throttle-debounce.js',
'targets' => [ 'desktop', 'mobile' ],
],
'actioncomplete',
],
],
+ 'mediawiki.page.rollback.confirmation' => [
+ 'scripts' => 'resources/src/mediawiki.rollback.confirmation.js',
+ 'dependencies' => [
+ 'jquery.confirmable'
+ ],
+ 'messages' => [
+ 'rollback-confirmation-confirm',
+ 'rollback-confirmation-yes',
+ 'rollback-confirmation-no',
+ ],
+ ],
'mediawiki.page.image.pagination' => [
'scripts' => 'resources/src/mediawiki.page.image.pagination.js',
'dependencies' => [
+++ /dev/null
-/*!
- * jQuery throttle / debounce - v1.1 - 3/7/2010
- * http://benalman.com/projects/jquery-throttle-debounce-plugin/
- *
- * Copyright (c) 2010 "Cowboy" Ben Alman
- * Dual licensed under the MIT and GPL licenses.
- * http://benalman.com/about/license/
- */
-
-// Script: jQuery throttle / debounce: Sometimes, less is more!
-//
-// *Version: 1.1, Last updated: 3/7/2010*
-//
-// Project Home - http://benalman.com/projects/jquery-throttle-debounce-plugin/
-// GitHub - http://github.com/cowboy/jquery-throttle-debounce/
-// Source - http://github.com/cowboy/jquery-throttle-debounce/raw/master/jquery.ba-throttle-debounce.js
-// (Minified) - http://github.com/cowboy/jquery-throttle-debounce/raw/master/jquery.ba-throttle-debounce.min.js (0.7kb)
-//
-// About: License
-//
-// Copyright (c) 2010 "Cowboy" Ben Alman,
-// Dual licensed under the MIT and GPL licenses.
-// http://benalman.com/about/license/
-//
-// About: Examples
-//
-// These working examples, complete with fully commented code, illustrate a few
-// ways in which this plugin can be used.
-//
-// Throttle - http://benalman.com/code/projects/jquery-throttle-debounce/examples/throttle/
-// Debounce - http://benalman.com/code/projects/jquery-throttle-debounce/examples/debounce/
-//
-// About: Support and Testing
-//
-// Information about what version or versions of jQuery this plugin has been
-// tested with, what browsers it has been tested in, and where the unit tests
-// reside (so you can test it yourself).
-//
-// jQuery Versions - none, 1.3.2, 1.4.2
-// Browsers Tested - Internet Explorer 6-8, Firefox 2-3.6, Safari 3-4, Chrome 4-5, Opera 9.6-10.1.
-// Unit Tests - http://benalman.com/code/projects/jquery-throttle-debounce/unit/
-//
-// About: Release History
-//
-// 1.1 - (3/7/2010) Fixed a bug in <jQuery.throttle> where trailing callbacks
-// executed later than they should. Reworked a fair amount of internal
-// logic as well.
-// 1.0 - (3/6/2010) Initial release as a stand-alone project. Migrated over
-// from jquery-misc repo v0.4 to jquery-throttle repo v1.0, added the
-// no_trailing throttle parameter and debounce functionality.
-//
-// Topic: Note for non-jQuery users
-//
-// jQuery isn't actually required for this plugin, because nothing internal
-// uses any jQuery methods or properties. jQuery is just used as a namespace
-// under which these methods can exist.
-//
-// Since jQuery isn't actually required for this plugin, if jQuery doesn't exist
-// when this plugin is loaded, the method described below will be created in
-// the `Cowboy` namespace. Usage will be exactly the same, but instead of
-// $.method() or jQuery.method(), you'll need to use Cowboy.method().
-
-(function(window,undefined){
- '$:nomunge'; // Used by YUI compressor.
-
- // Since jQuery really isn't required for this plugin, use `jQuery` as the
- // namespace only if it already exists, otherwise use the `Cowboy` namespace,
- // creating it if necessary.
- var $ = window.jQuery || window.Cowboy || ( window.Cowboy = {} ),
-
- // Internal method reference.
- jq_throttle;
-
- // Method: jQuery.throttle
- //
- // Throttle execution of a function. Especially useful for rate limiting
- // execution of handlers on events like resize and scroll. If you want to
- // rate-limit execution of a function to a single time, see the
- // <jQuery.debounce> method.
- //
- // In this visualization, | is a throttled-function call and X is the actual
- // callback execution:
- //
- // > Throttled with `no_trailing` specified as false or unspecified:
- // > ||||||||||||||||||||||||| (pause) |||||||||||||||||||||||||
- // > X X X X X X X X X X X X
- // >
- // > Throttled with `no_trailing` specified as true:
- // > ||||||||||||||||||||||||| (pause) |||||||||||||||||||||||||
- // > X X X X X X X X X X
- //
- // Usage:
- //
- // > var throttled = jQuery.throttle( delay, [ no_trailing, ] callback );
- // >
- // > jQuery('selector').bind( 'someevent', throttled );
- // > jQuery('selector').unbind( 'someevent', throttled );
- //
- // This also works in jQuery 1.4+:
- //
- // > jQuery('selector').bind( 'someevent', jQuery.throttle( delay, [ no_trailing, ] callback ) );
- // > jQuery('selector').unbind( 'someevent', callback );
- //
- // Arguments:
- //
- // delay - (Number) A zero-or-greater delay in milliseconds. For event
- // callbacks, values around 100 or 250 (or even higher) are most useful.
- // no_trailing - (Boolean) Optional, defaults to false. If no_trailing is
- // true, callback will only execute every `delay` milliseconds while the
- // throttled-function is being called. If no_trailing is false or
- // unspecified, callback will be executed one final time after the last
- // throttled-function call. (After the throttled-function has not been
- // called for `delay` milliseconds, the internal counter is reset)
- // callback - (Function) A function to be executed after delay milliseconds.
- // The `this` context and all arguments are passed through, as-is, to
- // `callback` when the throttled-function is executed.
- //
- // Returns:
- //
- // (Function) A new, throttled, function.
-
- $.throttle = jq_throttle = function( delay, no_trailing, callback, debounce_mode ) {
- // After wrapper has stopped being called, this timeout ensures that
- // `callback` is executed at the proper times in `throttle` and `end`
- // debounce modes.
- var timeout_id,
-
- // Keep track of the last time `callback` was executed.
- last_exec = 0;
-
- // `no_trailing` defaults to falsy.
- if ( typeof no_trailing !== 'boolean' ) {
- debounce_mode = callback;
- callback = no_trailing;
- no_trailing = undefined;
- }
-
- // The `wrapper` function encapsulates all of the throttling / debouncing
- // functionality and when executed will limit the rate at which `callback`
- // is executed.
- function wrapper() {
- var that = this,
- elapsed = +new Date() - last_exec,
- args = arguments;
-
- // Execute `callback` and update the `last_exec` timestamp.
- function exec() {
- last_exec = +new Date();
- callback.apply( that, args );
- };
-
- // If `debounce_mode` is true (at_begin) this is used to clear the flag
- // to allow future `callback` executions.
- function clear() {
- timeout_id = undefined;
- };
-
- if ( debounce_mode && !timeout_id ) {
- // Since `wrapper` is being called for the first time and
- // `debounce_mode` is true (at_begin), execute `callback`.
- exec();
- }
-
- // Clear any existing timeout.
- timeout_id && clearTimeout( timeout_id );
-
- if ( debounce_mode === undefined && elapsed > delay ) {
- // In throttle mode, if `delay` time has been exceeded, execute
- // `callback`.
- exec();
-
- } else if ( no_trailing !== true ) {
- // In trailing throttle mode, since `delay` time has not been
- // exceeded, schedule `callback` to execute `delay` ms after most
- // recent execution.
- //
- // If `debounce_mode` is true (at_begin), schedule `clear` to execute
- // after `delay` ms.
- //
- // If `debounce_mode` is false (at end), schedule `callback` to
- // execute after `delay` ms.
- timeout_id = setTimeout( debounce_mode ? clear : exec, debounce_mode === undefined ? delay - elapsed : delay );
- }
- };
-
- // Set the guid of `wrapper` function to the same of original callback, so
- // it can be removed in jQuery 1.4+ .unbind or .die by using the original
- // callback as a reference.
- if ( $.guid ) {
- wrapper.guid = callback.guid = callback.guid || $.guid++;
- }
-
- // Return the wrapper function.
- return wrapper;
- };
-
- // Method: jQuery.debounce
- //
- // Debounce execution of a function. Debouncing, unlike throttling,
- // guarantees that a function is only executed a single time, either at the
- // very beginning of a series of calls, or at the very end. If you want to
- // simply rate-limit execution of a function, see the <jQuery.throttle>
- // method.
- //
- // In this visualization, | is a debounced-function call and X is the actual
- // callback execution:
- //
- // > Debounced with `at_begin` specified as false or unspecified:
- // > ||||||||||||||||||||||||| (pause) |||||||||||||||||||||||||
- // > X X
- // >
- // > Debounced with `at_begin` specified as true:
- // > ||||||||||||||||||||||||| (pause) |||||||||||||||||||||||||
- // > X X
- //
- // Usage:
- //
- // > var debounced = jQuery.debounce( delay, [ at_begin, ] callback );
- // >
- // > jQuery('selector').bind( 'someevent', debounced );
- // > jQuery('selector').unbind( 'someevent', debounced );
- //
- // This also works in jQuery 1.4+:
- //
- // > jQuery('selector').bind( 'someevent', jQuery.debounce( delay, [ at_begin, ] callback ) );
- // > jQuery('selector').unbind( 'someevent', callback );
- //
- // Arguments:
- //
- // delay - (Number) A zero-or-greater delay in milliseconds. For event
- // callbacks, values around 100 or 250 (or even higher) are most useful.
- // at_begin - (Boolean) Optional, defaults to false. If at_begin is false or
- // unspecified, callback will only be executed `delay` milliseconds after
- // the last debounced-function call. If at_begin is true, callback will be
- // executed only at the first debounced-function call. (After the
- // throttled-function has not been called for `delay` milliseconds, the
- // internal counter is reset)
- // callback - (Function) A function to be executed after delay milliseconds.
- // The `this` context and all arguments are passed through, as-is, to
- // `callback` when the debounced-function is executed.
- //
- // Returns:
- //
- // (Function) A new, debounced, function.
-
- $.debounce = function( delay, at_begin, callback ) {
- return callback === undefined
- ? jq_throttle( delay, at_begin, false )
- : jq_throttle( delay, callback, at_begin !== false );
- };
-
-})(this);
+++ /dev/null
-/*!
- * jQuery Form Plugin
- * version: 3.14 (30-JUL-2012)
- * @requires jQuery v1.3.2 or later
- *
- * Examples and documentation at: http://malsup.com/jquery/form/
- * Project repository: https://github.com/malsup/form
- * Dual licensed under the MIT and GPL licenses:
- * http://malsup.github.com/mit-license.txt
- * http://malsup.github.com/gpl-license-v2.txt
- */
-/*global ActiveXObject alert */
-;(function($) {
-"use strict";
-
-/*
- Usage Note:
- -----------
- Do not use both ajaxSubmit and ajaxForm on the same form. These
- functions are mutually exclusive. Use ajaxSubmit if you want
- to bind your own submit handler to the form. For example,
-
- $(document).ready(function() {
- $('#myForm').on('submit', function(e) {
- e.preventDefault(); // <-- important
- $(this).ajaxSubmit({
- target: '#output'
- });
- });
- });
-
- Use ajaxForm when you want the plugin to manage all the event binding
- for you. For example,
-
- $(document).ready(function() {
- $('#myForm').ajaxForm({
- target: '#output'
- });
- });
-
- You can also use ajaxForm with delegation (requires jQuery v1.7+), so the
- form does not have to exist when you invoke ajaxForm:
-
- $('#myForm').ajaxForm({
- delegation: true,
- target: '#output'
- });
-
- When using ajaxForm, the ajaxSubmit function will be invoked for you
- at the appropriate time.
-*/
-
-/**
- * Feature detection
- */
-var feature = {};
-feature.fileapi = $("<input type='file'/>").get(0).files !== undefined;
-feature.formdata = window.FormData !== undefined;
-
-/**
- * ajaxSubmit() provides a mechanism for immediately submitting
- * an HTML form using AJAX.
- */
-$.fn.ajaxSubmit = function(options) {
- /*jshint scripturl:true */
-
- // fast fail if nothing selected (http://dev.jquery.com/ticket/2752)
- if (!this.length) {
- log('ajaxSubmit: skipping submit process - no element selected');
- return this;
- }
-
- var method, action, url, $form = this;
-
- if (typeof options == 'function') {
- options = { success: options };
- }
-
- method = this.attr('method');
- action = this.attr('action');
- url = (typeof action === 'string') ? $.trim(action) : '';
- url = url || window.location.href || '';
- if (url) {
- // clean url (don't include hash vaue)
- url = (url.match(/^([^#]+)/)||[])[1];
- }
-
- options = $.extend(true, {
- url: url,
- success: $.ajaxSettings.success,
- type: method || 'GET',
- iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank'
- }, options);
-
- // hook for manipulating the form data before it is extracted;
- // convenient for use with rich editors like tinyMCE or FCKEditor
- var veto = {};
- this.trigger('form-pre-serialize', [this, options, veto]);
- if (veto.veto) {
- log('ajaxSubmit: submit vetoed via form-pre-serialize trigger');
- return this;
- }
-
- // provide opportunity to alter form data before it is serialized
- if (options.beforeSerialize && options.beforeSerialize(this, options) === false) {
- log('ajaxSubmit: submit aborted via beforeSerialize callback');
- return this;
- }
-
- var traditional = options.traditional;
- if ( traditional === undefined ) {
- traditional = $.ajaxSettings.traditional;
- }
-
- var elements = [];
- var qx, a = this.formToArray(options.semantic, elements);
- if (options.data) {
- options.extraData = options.data;
- qx = $.param(options.data, traditional);
- }
-
- // give pre-submit callback an opportunity to abort the submit
- if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) {
- log('ajaxSubmit: submit aborted via beforeSubmit callback');
- return this;
- }
-
- // fire vetoable 'validate' event
- this.trigger('form-submit-validate', [a, this, options, veto]);
- if (veto.veto) {
- log('ajaxSubmit: submit vetoed via form-submit-validate trigger');
- return this;
- }
-
- var q = $.param(a, traditional);
- if (qx) {
- q = ( q ? (q + '&' + qx) : qx );
- }
- if (options.type.toUpperCase() == 'GET') {
- options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q;
- options.data = null; // data is null for 'get'
- }
- else {
- options.data = q; // data is the query string for 'post'
- }
-
- var callbacks = [];
- if (options.resetForm) {
- callbacks.push(function() { $form.resetForm(); });
- }
- if (options.clearForm) {
- callbacks.push(function() { $form.clearForm(options.includeHidden); });
- }
-
- // perform a load on the target only if dataType is not provided
- if (!options.dataType && options.target) {
- var oldSuccess = options.success || function(){};
- callbacks.push(function(data) {
- var fn = options.replaceTarget ? 'replaceWith' : 'html';
- $(options.target)[fn](data).each(oldSuccess, arguments);
- });
- }
- else if (options.success) {
- callbacks.push(options.success);
- }
-
- options.success = function(data, status, xhr) { // jQuery 1.4+ passes xhr as 3rd arg
- var context = options.context || this ; // jQuery 1.4+ supports scope context
- for (var i=0, max=callbacks.length; i < max; i++) {
- callbacks[i].apply(context, [data, status, xhr || $form, $form]);
- }
- };
-
- // are there files to upload?
- var fileInputs = $('input:file:enabled[value]', this); // [value] (issue #113)
- var hasFileInputs = fileInputs.length > 0;
- var mp = 'multipart/form-data';
- var multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp);
-
- var fileAPI = feature.fileapi && feature.formdata;
- log("fileAPI :" + fileAPI);
- var shouldUseFrame = (hasFileInputs || multipart) && !fileAPI;
-
- // options.iframe allows user to force iframe mode
- // 06-NOV-09: now defaulting to iframe mode if file input is detected
- if (options.iframe !== false && (options.iframe || shouldUseFrame)) {
- // hack to fix Safari hang (thanks to Tim Molendijk for this)
- // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d
- if (options.closeKeepAlive) {
- $.get(options.closeKeepAlive, function() {
- fileUploadIframe(a);
- });
- }
- else {
- fileUploadIframe(a);
- }
- }
- else if ((hasFileInputs || multipart) && fileAPI) {
- fileUploadXhr(a);
- }
- else {
- $.ajax(options);
- }
-
- // clear element array
- for (var k=0; k < elements.length; k++)
- elements[k] = null;
-
- // fire 'notify' event
- this.trigger('form-submit-notify', [this, options]);
- return this;
-
- // XMLHttpRequest Level 2 file uploads (big hat tip to francois2metz)
- function fileUploadXhr(a) {
- var formdata = new FormData();
-
- for (var i=0; i < a.length; i++) {
- formdata.append(a[i].name, a[i].value);
- }
-
- if (options.extraData) {
- for (var p in options.extraData)
- if (options.extraData.hasOwnProperty(p))
- formdata.append(p, options.extraData[p]);
- }
-
- options.data = null;
-
- var s = $.extend(true, {}, $.ajaxSettings, options, {
- contentType: false,
- processData: false,
- cache: false,
- type: 'POST'
- });
-
- if (options.uploadProgress) {
- // workaround because jqXHR does not expose upload property
- s.xhr = function() {
- var xhr = jQuery.ajaxSettings.xhr();
- if (xhr.upload) {
- xhr.upload.onprogress = function(event) {
- var percent = 0;
- var position = event.loaded || event.position; /*event.position is deprecated*/
- var total = event.total;
- if (event.lengthComputable) {
- percent = Math.ceil(position / total * 100);
- }
- options.uploadProgress(event, position, total, percent);
- };
- }
- return xhr;
- };
- }
-
- s.data = null;
- var beforeSend = s.beforeSend;
- s.beforeSend = function(xhr, o) {
- o.data = formdata;
- if(beforeSend)
- beforeSend.call(this, xhr, o);
- };
- $.ajax(s);
- }
-
- // private function for handling file uploads (hat tip to YAHOO!)
- function fileUploadIframe(a) {
- var form = $form[0], el, i, s, g, id, $io, io, xhr, sub, n, timedOut, timeoutHandle;
- var useProp = !!$.fn.prop;
-
- if ($(':input[name=submit],:input[id=submit]', form).length) {
- // if there is an input with a name or id of 'submit' then we won't be
- // able to invoke the submit fn on the form (at least not x-browser)
- alert('Error: Form elements must not have name or id of "submit".');
- return;
- }
-
- if (a) {
- // ensure that every serialized input is still enabled
- for (i=0; i < elements.length; i++) {
- el = $(elements[i]);
- if ( useProp )
- el.prop('disabled', false);
- else
- el.removeAttr('disabled');
- }
- }
-
- s = $.extend(true, {}, $.ajaxSettings, options);
- s.context = s.context || s;
- id = 'jqFormIO' + (new Date().getTime());
- if (s.iframeTarget) {
- $io = $(s.iframeTarget);
- n = $io.attr('name');
- if (!n)
- $io.attr('name', id);
- else
- id = n;
- }
- else {
- $io = $('<iframe name="' + id + '" src="'+ s.iframeSrc +'" />');
- $io.css({ position: 'absolute', top: '-1000px', left: '-1000px' });
- }
- io = $io[0];
-
-
- xhr = { // mock object
- aborted: 0,
- responseText: null,
- responseXML: null,
- status: 0,
- statusText: 'n/a',
- getAllResponseHeaders: function() {},
- getResponseHeader: function() {},
- setRequestHeader: function() {},
- abort: function(status) {
- var e = (status === 'timeout' ? 'timeout' : 'aborted');
- log('aborting upload... ' + e);
- this.aborted = 1;
- // #214
- if (io.contentWindow.document.execCommand) {
- try { // #214
- io.contentWindow.document.execCommand('Stop');
- } catch(ignore) {}
- }
- $io.attr('src', s.iframeSrc); // abort op in progress
- xhr.error = e;
- if (s.error)
- s.error.call(s.context, xhr, e, status);
- if (g)
- $.event.trigger("ajaxError", [xhr, s, e]);
- if (s.complete)
- s.complete.call(s.context, xhr, e);
- }
- };
-
- g = s.global;
- // trigger ajax global events so that activity/block indicators work like normal
- if (g && 0 === $.active++) {
- $.event.trigger("ajaxStart");
- }
- if (g) {
- $.event.trigger("ajaxSend", [xhr, s]);
- }
-
- if (s.beforeSend && s.beforeSend.call(s.context, xhr, s) === false) {
- if (s.global) {
- $.active--;
- }
- return;
- }
- if (xhr.aborted) {
- return;
- }
-
- // add submitting element to data if we know it
- sub = form.clk;
- if (sub) {
- n = sub.name;
- if (n && !sub.disabled) {
- s.extraData = s.extraData || {};
- s.extraData[n] = sub.value;
- if (sub.type == "image") {
- s.extraData[n+'.x'] = form.clk_x;
- s.extraData[n+'.y'] = form.clk_y;
- }
- }
- }
-
- var CLIENT_TIMEOUT_ABORT = 1;
- var SERVER_ABORT = 2;
-
- function getDoc(frame) {
- var doc = frame.contentWindow ? frame.contentWindow.document : frame.contentDocument ? frame.contentDocument : frame.document;
- return doc;
- }
-
- // Rails CSRF hack (thanks to Yvan Barthelemy)
- var csrf_token = $('meta[name=csrf-token]').attr('content');
- var csrf_param = $('meta[name=csrf-param]').attr('content');
- if (csrf_param && csrf_token) {
- s.extraData = s.extraData || {};
- s.extraData[csrf_param] = csrf_token;
- }
-
- // take a breath so that pending repaints get some cpu time before the upload starts
- function doSubmit() {
- // make sure form attrs are set
- var t = $form.attr('target'), a = $form.attr('action');
-
- // update form attrs in IE friendly way
- form.setAttribute('target',id);
- if (!method) {
- form.setAttribute('method', 'POST');
- }
- if (a != s.url) {
- form.setAttribute('action', s.url);
- }
-
- // ie borks in some cases when setting encoding
- if (! s.skipEncodingOverride && (!method || /post/i.test(method))) {
- $form.attr({
- encoding: 'multipart/form-data',
- enctype: 'multipart/form-data'
- });
- }
-
- // support timout
- if (s.timeout) {
- timeoutHandle = setTimeout(function() { timedOut = true; cb(CLIENT_TIMEOUT_ABORT); }, s.timeout);
- }
-
- // look for server aborts
- function checkState() {
- try {
- var state = getDoc(io).readyState;
- log('state = ' + state);
- if (state && state.toLowerCase() == 'uninitialized')
- setTimeout(checkState,50);
- }
- catch(e) {
- log('Server abort: ' , e, ' (', e.name, ')');
- cb(SERVER_ABORT);
- if (timeoutHandle)
- clearTimeout(timeoutHandle);
- timeoutHandle = undefined;
- }
- }
-
- // add "extra" data to form if provided in options
- var extraInputs = [];
- try {
- if (s.extraData) {
- for (var n in s.extraData) {
- if (s.extraData.hasOwnProperty(n)) {
- // if using the $.param format that allows for multiple values with the same name
- if($.isPlainObject(s.extraData[n]) && s.extraData[n].hasOwnProperty('name') && s.extraData[n].hasOwnProperty('value')) {
- extraInputs.push(
- $('<input type="hidden" name="'+s.extraData[n].name+'">').attr('value',s.extraData[n].value)
- .appendTo(form)[0]);
- } else {
- extraInputs.push(
- $('<input type="hidden" name="'+n+'">').attr('value',s.extraData[n])
- .appendTo(form)[0]);
- }
- }
- }
- }
-
- if (!s.iframeTarget) {
- // add iframe to doc and submit the form
- $io.appendTo('body');
- if (io.attachEvent)
- io.attachEvent('onload', cb);
- else
- io.addEventListener('load', cb, false);
- }
- setTimeout(checkState,15);
- form.submit();
- }
- finally {
- // reset attrs and remove "extra" input elements
- form.setAttribute('action',a);
- if(t) {
- form.setAttribute('target', t);
- } else {
- $form.removeAttr('target');
- }
- $(extraInputs).remove();
- }
- }
-
- if (s.forceSync) {
- doSubmit();
- }
- else {
- setTimeout(doSubmit, 10); // this lets dom updates render
- }
-
- var data, doc, domCheckCount = 50, callbackProcessed;
-
- function cb(e) {
- if (xhr.aborted || callbackProcessed) {
- return;
- }
- try {
- doc = getDoc(io);
- }
- catch(ex) {
- log('cannot access response document: ', ex);
- e = SERVER_ABORT;
- }
- if (e === CLIENT_TIMEOUT_ABORT && xhr) {
- xhr.abort('timeout');
- return;
- }
- else if (e == SERVER_ABORT && xhr) {
- xhr.abort('server abort');
- return;
- }
-
- if (!doc || doc.location.href == s.iframeSrc) {
- // response not received yet
- if (!timedOut)
- return;
- }
- if (io.detachEvent)
- io.detachEvent('onload', cb);
- else
- io.removeEventListener('load', cb, false);
-
- var status = 'success', errMsg;
- try {
- if (timedOut) {
- throw 'timeout';
- }
-
- var isXml = s.dataType == 'xml' || doc.XMLDocument || $.isXMLDoc(doc);
- log('isXml='+isXml);
- if (!isXml && window.opera && (doc.body === null || !doc.body.innerHTML)) {
- if (--domCheckCount) {
- // in some browsers (Opera) the iframe DOM is not always traversable when
- // the onload callback fires, so we loop a bit to accommodate
- log('requeing onLoad callback, DOM not available');
- setTimeout(cb, 250);
- return;
- }
- // let this fall through because server response could be an empty document
- //log('Could not access iframe DOM after mutiple tries.');
- //throw 'DOMException: not available';
- }
-
- //log('response detected');
- var docRoot = doc.body ? doc.body : doc.documentElement;
- xhr.responseText = docRoot ? docRoot.innerHTML : null;
- xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc;
- if (isXml)
- s.dataType = 'xml';
- xhr.getResponseHeader = function(header){
- var headers = {'content-type': s.dataType};
- return headers[header];
- };
- // support for XHR 'status' & 'statusText' emulation :
- if (docRoot) {
- xhr.status = Number( docRoot.getAttribute('status') ) || xhr.status;
- xhr.statusText = docRoot.getAttribute('statusText') || xhr.statusText;
- }
-
- var dt = (s.dataType || '').toLowerCase();
- var scr = /(json|script|text)/.test(dt);
- if (scr || s.textarea) {
- // see if user embedded response in textarea
- var ta = doc.getElementsByTagName('textarea')[0];
- if (ta) {
- xhr.responseText = ta.value;
- // support for XHR 'status' & 'statusText' emulation :
- xhr.status = Number( ta.getAttribute('status') ) || xhr.status;
- xhr.statusText = ta.getAttribute('statusText') || xhr.statusText;
- }
- else if (scr) {
- // account for browsers injecting pre around json response
- var pre = doc.getElementsByTagName('pre')[0];
- var b = doc.getElementsByTagName('body')[0];
- if (pre) {
- xhr.responseText = pre.textContent ? pre.textContent : pre.innerText;
- }
- else if (b) {
- xhr.responseText = b.textContent ? b.textContent : b.innerText;
- }
- }
- }
- else if (dt == 'xml' && !xhr.responseXML && xhr.responseText) {
- xhr.responseXML = toXml(xhr.responseText);
- }
-
- try {
- data = httpData(xhr, dt, s);
- }
- catch (e) {
- status = 'parsererror';
- xhr.error = errMsg = (e || status);
- }
- }
- catch (e) {
- log('error caught: ',e);
- status = 'error';
- xhr.error = errMsg = (e || status);
- }
-
- if (xhr.aborted) {
- log('upload aborted');
- status = null;
- }
-
- if (xhr.status) { // we've set xhr.status
- status = (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) ? 'success' : 'error';
- }
-
- // ordering of these callbacks/triggers is odd, but that's how $.ajax does it
- if (status === 'success') {
- if (s.success)
- s.success.call(s.context, data, 'success', xhr);
- if (g)
- $.event.trigger("ajaxSuccess", [xhr, s]);
- }
- else if (status) {
- if (errMsg === undefined)
- errMsg = xhr.statusText;
- if (s.error)
- s.error.call(s.context, xhr, status, errMsg);
- if (g)
- $.event.trigger("ajaxError", [xhr, s, errMsg]);
- }
-
- if (g)
- $.event.trigger("ajaxComplete", [xhr, s]);
-
- if (g && ! --$.active) {
- $.event.trigger("ajaxStop");
- }
-
- if (s.complete)
- s.complete.call(s.context, xhr, status);
-
- callbackProcessed = true;
- if (s.timeout)
- clearTimeout(timeoutHandle);
-
- // clean up
- setTimeout(function() {
- if (!s.iframeTarget)
- $io.remove();
- xhr.responseXML = null;
- }, 100);
- }
-
- var toXml = $.parseXML || function(s, doc) { // use parseXML if available (jQuery 1.5+)
- if (window.ActiveXObject) {
- doc = new ActiveXObject('Microsoft.XMLDOM');
- doc.async = 'false';
- doc.loadXML(s);
- }
- else {
- doc = (new DOMParser()).parseFromString(s, 'text/xml');
- }
- return (doc && doc.documentElement && doc.documentElement.nodeName != 'parsererror') ? doc : null;
- };
- var parseJSON = $.parseJSON || function(s) {
- /*jslint evil:true */
- return window['eval']('(' + s + ')');
- };
-
- var httpData = function( xhr, type, s ) { // mostly lifted from jq1.4.4
-
- var ct = xhr.getResponseHeader('content-type') || '',
- xml = type === 'xml' || !type && ct.indexOf('xml') >= 0,
- data = xml ? xhr.responseXML : xhr.responseText;
-
- if (xml && data.documentElement.nodeName === 'parsererror') {
- if ($.error)
- $.error('parsererror');
- }
- if (s && s.dataFilter) {
- data = s.dataFilter(data, type);
- }
- if (typeof data === 'string') {
- if (type === 'json' || !type && ct.indexOf('json') >= 0) {
- data = parseJSON(data);
- } else if (type === "script" || !type && ct.indexOf("javascript") >= 0) {
- $.globalEval(data);
- }
- }
- return data;
- };
- }
-};
-
-/**
- * ajaxForm() provides a mechanism for fully automating form submission.
- *
- * The advantages of using this method instead of ajaxSubmit() are:
- *
- * 1: This method will include coordinates for <input type="image" /> elements (if the element
- * is used to submit the form).
- * 2. This method will include the submit element's name/value data (for the element that was
- * used to submit the form).
- * 3. This method binds the submit() method to the form for you.
- *
- * The options argument for ajaxForm works exactly as it does for ajaxSubmit. ajaxForm merely
- * passes the options argument along after properly binding events for submit elements and
- * the form itself.
- */
-$.fn.ajaxForm = function(options) {
- options = options || {};
- options.delegation = options.delegation && $.isFunction($.fn.on);
-
- // in jQuery 1.3+ we can fix mistakes with the ready state
- if (!options.delegation && this.length === 0) {
- var o = { s: this.selector, c: this.context };
- if (!$.isReady && o.s) {
- log('DOM not ready, queuing ajaxForm');
- $(function() {
- $(o.s,o.c).ajaxForm(options);
- });
- return this;
- }
- // is your DOM ready? http://docs.jquery.com/Tutorials:Introducing_$(document).ready()
- log('terminating; zero elements found by selector' + ($.isReady ? '' : ' (DOM not ready)'));
- return this;
- }
-
- if ( options.delegation ) {
- $(document)
- .off('submit.form-plugin', this.selector, doAjaxSubmit)
- .off('click.form-plugin', this.selector, captureSubmittingElement)
- .on('submit.form-plugin', this.selector, options, doAjaxSubmit)
- .on('click.form-plugin', this.selector, options, captureSubmittingElement);
- return this;
- }
-
- return this.ajaxFormUnbind()
- .bind('submit.form-plugin', options, doAjaxSubmit)
- .bind('click.form-plugin', options, captureSubmittingElement);
-};
-
-// private event handlers
-function doAjaxSubmit(e) {
- /*jshint validthis:true */
- var options = e.data;
- if (!e.isDefaultPrevented()) { // if event has been canceled, don't proceed
- e.preventDefault();
- $(this).ajaxSubmit(options);
- }
-}
-
-function captureSubmittingElement(e) {
- /*jshint validthis:true */
- var target = e.target;
- var $el = $(target);
- if (!($el.is(":submit,input:image"))) {
- // is this a child element of the submit el? (ex: a span within a button)
- var t = $el.closest(':submit');
- if (t.length === 0) {
- return;
- }
- target = t[0];
- }
- var form = this;
- form.clk = target;
- if (target.type == 'image') {
- if (e.offsetX !== undefined) {
- form.clk_x = e.offsetX;
- form.clk_y = e.offsetY;
- } else if (typeof $.fn.offset == 'function') {
- var offset = $el.offset();
- form.clk_x = e.pageX - offset.left;
- form.clk_y = e.pageY - offset.top;
- } else {
- form.clk_x = e.pageX - target.offsetLeft;
- form.clk_y = e.pageY - target.offsetTop;
- }
- }
- // clear form vars
- setTimeout(function() { form.clk = form.clk_x = form.clk_y = null; }, 100);
-}
-
-
-// ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm
-$.fn.ajaxFormUnbind = function() {
- return this.unbind('submit.form-plugin click.form-plugin');
-};
-
-/**
- * formToArray() gathers form element data into an array of objects that can
- * be passed to any of the following ajax functions: $.get, $.post, or load.
- * Each object in the array has both a 'name' and 'value' property. An example of
- * an array for a simple login form might be:
- *
- * [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ]
- *
- * It is this array that is passed to pre-submit callback functions provided to the
- * ajaxSubmit() and ajaxForm() methods.
- */
-$.fn.formToArray = function(semantic, elements) {
- var a = [];
- if (this.length === 0) {
- return a;
- }
-
- var form = this[0];
- var els = semantic ? form.getElementsByTagName('*') : form.elements;
- if (!els) {
- return a;
- }
-
- var i,j,n,v,el,max,jmax;
- for(i=0, max=els.length; i < max; i++) {
- el = els[i];
- n = el.name;
- if (!n) {
- continue;
- }
-
- if (semantic && form.clk && el.type == "image") {
- // handle image inputs on the fly when semantic == true
- if(!el.disabled && form.clk == el) {
- a.push({name: n, value: $(el).val(), type: el.type });
- a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
- }
- continue;
- }
-
- v = $.fieldValue(el, true);
- if (v && v.constructor == Array) {
- if (elements)
- elements.push(el);
- for(j=0, jmax=v.length; j < jmax; j++) {
- a.push({name: n, value: v[j]});
- }
- }
- else if (feature.fileapi && el.type == 'file' && !el.disabled) {
- if (elements)
- elements.push(el);
- var files = el.files;
- if (files.length) {
- for (j=0; j < files.length; j++) {
- a.push({name: n, value: files[j], type: el.type});
- }
- }
- else {
- // #180
- a.push({ name: n, value: '', type: el.type });
- }
- }
- else if (v !== null && typeof v != 'undefined') {
- if (elements)
- elements.push(el);
- a.push({name: n, value: v, type: el.type, required: el.required});
- }
- }
-
- if (!semantic && form.clk) {
- // input type=='image' are not found in elements array! handle it here
- var $input = $(form.clk), input = $input[0];
- n = input.name;
- if (n && !input.disabled && input.type == 'image') {
- a.push({name: n, value: $input.val()});
- a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
- }
- }
- return a;
-};
-
-/**
- * Serializes form data into a 'submittable' string. This method will return a string
- * in the format: name1=value1&name2=value2
- */
-$.fn.formSerialize = function(semantic) {
- //hand off to jQuery.param for proper encoding
- return $.param(this.formToArray(semantic));
-};
-
-/**
- * Serializes all field elements in the jQuery object into a query string.
- * This method will return a string in the format: name1=value1&name2=value2
- */
-$.fn.fieldSerialize = function(successful) {
- var a = [];
- this.each(function() {
- var n = this.name;
- if (!n) {
- return;
- }
- var v = $.fieldValue(this, successful);
- if (v && v.constructor == Array) {
- for (var i=0,max=v.length; i < max; i++) {
- a.push({name: n, value: v[i]});
- }
- }
- else if (v !== null && typeof v != 'undefined') {
- a.push({name: this.name, value: v});
- }
- });
- //hand off to jQuery.param for proper encoding
- return $.param(a);
-};
-
-/**
- * Returns the value(s) of the element in the matched set. For example, consider the following form:
- *
- * <form><fieldset>
- * <input name="A" type="text" />
- * <input name="A" type="text" />
- * <input name="B" type="checkbox" value="B1" />
- * <input name="B" type="checkbox" value="B2"/>
- * <input name="C" type="radio" value="C1" />
- * <input name="C" type="radio" value="C2" />
- * </fieldset></form>
- *
- * var v = $(':text').fieldValue();
- * // if no values are entered into the text inputs
- * v == ['','']
- * // if values entered into the text inputs are 'foo' and 'bar'
- * v == ['foo','bar']
- *
- * var v = $(':checkbox').fieldValue();
- * // if neither checkbox is checked
- * v === undefined
- * // if both checkboxes are checked
- * v == ['B1', 'B2']
- *
- * var v = $(':radio').fieldValue();
- * // if neither radio is checked
- * v === undefined
- * // if first radio is checked
- * v == ['C1']
- *
- * The successful argument controls whether or not the field element must be 'successful'
- * (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls).
- * The default value of the successful argument is true. If this value is false the value(s)
- * for each element is returned.
- *
- * Note: This method *always* returns an array. If no valid value can be determined the
- * array will be empty, otherwise it will contain one or more values.
- */
-$.fn.fieldValue = function(successful) {
- for (var val=[], i=0, max=this.length; i < max; i++) {
- var el = this[i];
- var v = $.fieldValue(el, successful);
- if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length)) {
- continue;
- }
- if (v.constructor == Array)
- $.merge(val, v);
- else
- val.push(v);
- }
- return val;
-};
-
-/**
- * Returns the value of the field element.
- */
-$.fieldValue = function(el, successful) {
- var n = el.name, t = el.type, tag = el.tagName.toLowerCase();
- if (successful === undefined) {
- successful = true;
- }
-
- if (successful && (!n || el.disabled || t == 'reset' || t == 'button' ||
- (t == 'checkbox' || t == 'radio') && !el.checked ||
- (t == 'submit' || t == 'image') && el.form && el.form.clk != el ||
- tag == 'select' && el.selectedIndex == -1)) {
- return null;
- }
-
- if (tag == 'select') {
- var index = el.selectedIndex;
- if (index < 0) {
- return null;
- }
- var a = [], ops = el.options;
- var one = (t == 'select-one');
- var max = (one ? index+1 : ops.length);
- for(var i=(one ? index : 0); i < max; i++) {
- var op = ops[i];
- if (op.selected) {
- var v = op.value;
- if (!v) { // extra pain for IE...
- v = (op.attributes && op.attributes['value'] && !(op.attributes['value'].specified)) ? op.text : op.value;
- }
- if (one) {
- return v;
- }
- a.push(v);
- }
- }
- return a;
- }
- return $(el).val();
-};
-
-/**
- * Clears the form data. Takes the following actions on the form's input fields:
- * - input text fields will have their 'value' property set to the empty string
- * - select elements will have their 'selectedIndex' property set to -1
- * - checkbox and radio inputs will have their 'checked' property set to false
- * - inputs of type submit, button, reset, and hidden will *not* be effected
- * - button elements will *not* be effected
- */
-$.fn.clearForm = function(includeHidden) {
- return this.each(function() {
- $('input,select,textarea', this).clearFields(includeHidden);
- });
-};
-
-/**
- * Clears the selected form elements.
- */
-$.fn.clearFields = $.fn.clearInputs = function(includeHidden) {
- var re = /^(?:color|date|datetime|email|month|number|password|range|search|tel|text|time|url|week)$/i; // 'hidden' is not in this list
- return this.each(function() {
- var t = this.type, tag = this.tagName.toLowerCase();
- if (re.test(t) || tag == 'textarea') {
- this.value = '';
- }
- else if (t == 'checkbox' || t == 'radio') {
- this.checked = false;
- }
- else if (tag == 'select') {
- this.selectedIndex = -1;
- }
- else if (includeHidden) {
- // includeHidden can be the value true, or it can be a selector string
- // indicating a special test; for example:
- // $('#myForm').clearForm('.special:hidden')
- // the above would clean hidden inputs that have the class of 'special'
- if ( (includeHidden === true && /hidden/.test(t)) ||
- (typeof includeHidden == 'string' && $(this).is(includeHidden)) )
- this.value = '';
- }
- });
-};
-
-/**
- * Resets the form data. Causes all form elements to be reset to their original value.
- */
-$.fn.resetForm = function() {
- return this.each(function() {
- // guard against an input with the name of 'reset'
- // note that IE reports the reset function as an 'object'
- if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType)) {
- this.reset();
- }
- });
-};
-
-/**
- * Enables or disables any matching elements.
- */
-$.fn.enable = function(b) {
- if (b === undefined) {
- b = true;
- }
- return this.each(function() {
- this.disabled = !b;
- });
-};
-
-/**
- * Checks/unchecks any matching checkboxes or radio buttons and
- * selects/deselects and matching option elements.
- */
-$.fn.selected = function(select) {
- if (select === undefined) {
- select = true;
- }
- return this.each(function() {
- var t = this.type;
- if (t == 'checkbox' || t == 'radio') {
- this.checked = select;
- }
- else if (this.tagName.toLowerCase() == 'option') {
- var $sel = $(this).parent('select');
- if (select && $sel[0] && $sel[0].type == 'select-one') {
- // deselect all other options
- $sel.find('option').selected(false);
- }
- this.selected = select;
- }
- });
-};
-
-// expose debug var
-$.fn.ajaxSubmit.debug = false;
-
-// helper fn for console logging
-function log() {
- if (!$.fn.ajaxSubmit.debug)
- return;
- var msg = '[jquery.form] ' + Array.prototype.join.call(arguments,'');
- if (window.console && window.console.log) {
- window.console.log(msg);
- }
- else if (window.opera && window.opera.postError) {
- window.opera.postError(msg);
- }
-}
-
-})(jQuery);
--- /dev/null
+/*!
+ * jQuery Form Plugin
+ * version: 3.14 (30-JUL-2012)
+ * @requires jQuery v1.3.2 or later
+ *
+ * Examples and documentation at: http://malsup.com/jquery/form/
+ * Project repository: https://github.com/malsup/form
+ * Dual licensed under the MIT and GPL licenses:
+ * http://malsup.github.com/mit-license.txt
+ * http://malsup.github.com/gpl-license-v2.txt
+ */
+/*global ActiveXObject alert */
+;(function($) {
+"use strict";
+
+/*
+ Usage Note:
+ -----------
+ Do not use both ajaxSubmit and ajaxForm on the same form. These
+ functions are mutually exclusive. Use ajaxSubmit if you want
+ to bind your own submit handler to the form. For example,
+
+ $(document).ready(function() {
+ $('#myForm').on('submit', function(e) {
+ e.preventDefault(); // <-- important
+ $(this).ajaxSubmit({
+ target: '#output'
+ });
+ });
+ });
+
+ Use ajaxForm when you want the plugin to manage all the event binding
+ for you. For example,
+
+ $(document).ready(function() {
+ $('#myForm').ajaxForm({
+ target: '#output'
+ });
+ });
+
+ You can also use ajaxForm with delegation (requires jQuery v1.7+), so the
+ form does not have to exist when you invoke ajaxForm:
+
+ $('#myForm').ajaxForm({
+ delegation: true,
+ target: '#output'
+ });
+
+ When using ajaxForm, the ajaxSubmit function will be invoked for you
+ at the appropriate time.
+*/
+
+/**
+ * Feature detection
+ */
+var feature = {};
+feature.fileapi = $("<input type='file'/>").get(0).files !== undefined;
+feature.formdata = window.FormData !== undefined;
+
+/**
+ * ajaxSubmit() provides a mechanism for immediately submitting
+ * an HTML form using AJAX.
+ */
+$.fn.ajaxSubmit = function(options) {
+ /*jshint scripturl:true */
+
+ // fast fail if nothing selected (http://dev.jquery.com/ticket/2752)
+ if (!this.length) {
+ log('ajaxSubmit: skipping submit process - no element selected');
+ return this;
+ }
+
+ var method, action, url, $form = this;
+
+ if (typeof options == 'function') {
+ options = { success: options };
+ }
+
+ method = this.attr('method');
+ action = this.attr('action');
+ url = (typeof action === 'string') ? $.trim(action) : '';
+ url = url || window.location.href || '';
+ if (url) {
+ // clean url (don't include hash vaue)
+ url = (url.match(/^([^#]+)/)||[])[1];
+ }
+
+ options = $.extend(true, {
+ url: url,
+ success: $.ajaxSettings.success,
+ type: method || 'GET',
+ iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank'
+ }, options);
+
+ // hook for manipulating the form data before it is extracted;
+ // convenient for use with rich editors like tinyMCE or FCKEditor
+ var veto = {};
+ this.trigger('form-pre-serialize', [this, options, veto]);
+ if (veto.veto) {
+ log('ajaxSubmit: submit vetoed via form-pre-serialize trigger');
+ return this;
+ }
+
+ // provide opportunity to alter form data before it is serialized
+ if (options.beforeSerialize && options.beforeSerialize(this, options) === false) {
+ log('ajaxSubmit: submit aborted via beforeSerialize callback');
+ return this;
+ }
+
+ var traditional = options.traditional;
+ if ( traditional === undefined ) {
+ traditional = $.ajaxSettings.traditional;
+ }
+
+ var elements = [];
+ var qx, a = this.formToArray(options.semantic, elements);
+ if (options.data) {
+ options.extraData = options.data;
+ qx = $.param(options.data, traditional);
+ }
+
+ // give pre-submit callback an opportunity to abort the submit
+ if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) {
+ log('ajaxSubmit: submit aborted via beforeSubmit callback');
+ return this;
+ }
+
+ // fire vetoable 'validate' event
+ this.trigger('form-submit-validate', [a, this, options, veto]);
+ if (veto.veto) {
+ log('ajaxSubmit: submit vetoed via form-submit-validate trigger');
+ return this;
+ }
+
+ var q = $.param(a, traditional);
+ if (qx) {
+ q = ( q ? (q + '&' + qx) : qx );
+ }
+ if (options.type.toUpperCase() == 'GET') {
+ options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q;
+ options.data = null; // data is null for 'get'
+ }
+ else {
+ options.data = q; // data is the query string for 'post'
+ }
+
+ var callbacks = [];
+ if (options.resetForm) {
+ callbacks.push(function() { $form.resetForm(); });
+ }
+ if (options.clearForm) {
+ callbacks.push(function() { $form.clearForm(options.includeHidden); });
+ }
+
+ // perform a load on the target only if dataType is not provided
+ if (!options.dataType && options.target) {
+ var oldSuccess = options.success || function(){};
+ callbacks.push(function(data) {
+ var fn = options.replaceTarget ? 'replaceWith' : 'html';
+ $(options.target)[fn](data).each(oldSuccess, arguments);
+ });
+ }
+ else if (options.success) {
+ callbacks.push(options.success);
+ }
+
+ options.success = function(data, status, xhr) { // jQuery 1.4+ passes xhr as 3rd arg
+ var context = options.context || this ; // jQuery 1.4+ supports scope context
+ for (var i=0, max=callbacks.length; i < max; i++) {
+ callbacks[i].apply(context, [data, status, xhr || $form, $form]);
+ }
+ };
+
+ // are there files to upload?
+ var fileInputs = $('input:file:enabled[value]', this); // [value] (issue #113)
+ var hasFileInputs = fileInputs.length > 0;
+ var mp = 'multipart/form-data';
+ var multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp);
+
+ var fileAPI = feature.fileapi && feature.formdata;
+ log("fileAPI :" + fileAPI);
+ var shouldUseFrame = (hasFileInputs || multipart) && !fileAPI;
+
+ // options.iframe allows user to force iframe mode
+ // 06-NOV-09: now defaulting to iframe mode if file input is detected
+ if (options.iframe !== false && (options.iframe || shouldUseFrame)) {
+ // hack to fix Safari hang (thanks to Tim Molendijk for this)
+ // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d
+ if (options.closeKeepAlive) {
+ $.get(options.closeKeepAlive, function() {
+ fileUploadIframe(a);
+ });
+ }
+ else {
+ fileUploadIframe(a);
+ }
+ }
+ else if ((hasFileInputs || multipart) && fileAPI) {
+ fileUploadXhr(a);
+ }
+ else {
+ $.ajax(options);
+ }
+
+ // clear element array
+ for (var k=0; k < elements.length; k++)
+ elements[k] = null;
+
+ // fire 'notify' event
+ this.trigger('form-submit-notify', [this, options]);
+ return this;
+
+ // XMLHttpRequest Level 2 file uploads (big hat tip to francois2metz)
+ function fileUploadXhr(a) {
+ var formdata = new FormData();
+
+ for (var i=0; i < a.length; i++) {
+ formdata.append(a[i].name, a[i].value);
+ }
+
+ if (options.extraData) {
+ for (var p in options.extraData)
+ if (options.extraData.hasOwnProperty(p))
+ formdata.append(p, options.extraData[p]);
+ }
+
+ options.data = null;
+
+ var s = $.extend(true, {}, $.ajaxSettings, options, {
+ contentType: false,
+ processData: false,
+ cache: false,
+ type: 'POST'
+ });
+
+ if (options.uploadProgress) {
+ // workaround because jqXHR does not expose upload property
+ s.xhr = function() {
+ var xhr = jQuery.ajaxSettings.xhr();
+ if (xhr.upload) {
+ xhr.upload.onprogress = function(event) {
+ var percent = 0;
+ var position = event.loaded || event.position; /*event.position is deprecated*/
+ var total = event.total;
+ if (event.lengthComputable) {
+ percent = Math.ceil(position / total * 100);
+ }
+ options.uploadProgress(event, position, total, percent);
+ };
+ }
+ return xhr;
+ };
+ }
+
+ s.data = null;
+ var beforeSend = s.beforeSend;
+ s.beforeSend = function(xhr, o) {
+ o.data = formdata;
+ if(beforeSend)
+ beforeSend.call(this, xhr, o);
+ };
+ $.ajax(s);
+ }
+
+ // private function for handling file uploads (hat tip to YAHOO!)
+ function fileUploadIframe(a) {
+ var form = $form[0], el, i, s, g, id, $io, io, xhr, sub, n, timedOut, timeoutHandle;
+ var useProp = !!$.fn.prop;
+
+ if ($(':input[name=submit],:input[id=submit]', form).length) {
+ // if there is an input with a name or id of 'submit' then we won't be
+ // able to invoke the submit fn on the form (at least not x-browser)
+ alert('Error: Form elements must not have name or id of "submit".');
+ return;
+ }
+
+ if (a) {
+ // ensure that every serialized input is still enabled
+ for (i=0; i < elements.length; i++) {
+ el = $(elements[i]);
+ if ( useProp )
+ el.prop('disabled', false);
+ else
+ el.removeAttr('disabled');
+ }
+ }
+
+ s = $.extend(true, {}, $.ajaxSettings, options);
+ s.context = s.context || s;
+ id = 'jqFormIO' + (new Date().getTime());
+ if (s.iframeTarget) {
+ $io = $(s.iframeTarget);
+ n = $io.attr('name');
+ if (!n)
+ $io.attr('name', id);
+ else
+ id = n;
+ }
+ else {
+ $io = $('<iframe name="' + id + '" src="'+ s.iframeSrc +'" />');
+ $io.css({ position: 'absolute', top: '-1000px', left: '-1000px' });
+ }
+ io = $io[0];
+
+
+ xhr = { // mock object
+ aborted: 0,
+ responseText: null,
+ responseXML: null,
+ status: 0,
+ statusText: 'n/a',
+ getAllResponseHeaders: function() {},
+ getResponseHeader: function() {},
+ setRequestHeader: function() {},
+ abort: function(status) {
+ var e = (status === 'timeout' ? 'timeout' : 'aborted');
+ log('aborting upload... ' + e);
+ this.aborted = 1;
+ // #214
+ if (io.contentWindow.document.execCommand) {
+ try { // #214
+ io.contentWindow.document.execCommand('Stop');
+ } catch(ignore) {}
+ }
+ $io.attr('src', s.iframeSrc); // abort op in progress
+ xhr.error = e;
+ if (s.error)
+ s.error.call(s.context, xhr, e, status);
+ if (g)
+ $.event.trigger("ajaxError", [xhr, s, e]);
+ if (s.complete)
+ s.complete.call(s.context, xhr, e);
+ }
+ };
+
+ g = s.global;
+ // trigger ajax global events so that activity/block indicators work like normal
+ if (g && 0 === $.active++) {
+ $.event.trigger("ajaxStart");
+ }
+ if (g) {
+ $.event.trigger("ajaxSend", [xhr, s]);
+ }
+
+ if (s.beforeSend && s.beforeSend.call(s.context, xhr, s) === false) {
+ if (s.global) {
+ $.active--;
+ }
+ return;
+ }
+ if (xhr.aborted) {
+ return;
+ }
+
+ // add submitting element to data if we know it
+ sub = form.clk;
+ if (sub) {
+ n = sub.name;
+ if (n && !sub.disabled) {
+ s.extraData = s.extraData || {};
+ s.extraData[n] = sub.value;
+ if (sub.type == "image") {
+ s.extraData[n+'.x'] = form.clk_x;
+ s.extraData[n+'.y'] = form.clk_y;
+ }
+ }
+ }
+
+ var CLIENT_TIMEOUT_ABORT = 1;
+ var SERVER_ABORT = 2;
+
+ function getDoc(frame) {
+ var doc = frame.contentWindow ? frame.contentWindow.document : frame.contentDocument ? frame.contentDocument : frame.document;
+ return doc;
+ }
+
+ // Rails CSRF hack (thanks to Yvan Barthelemy)
+ var csrf_token = $('meta[name=csrf-token]').attr('content');
+ var csrf_param = $('meta[name=csrf-param]').attr('content');
+ if (csrf_param && csrf_token) {
+ s.extraData = s.extraData || {};
+ s.extraData[csrf_param] = csrf_token;
+ }
+
+ // take a breath so that pending repaints get some cpu time before the upload starts
+ function doSubmit() {
+ // make sure form attrs are set
+ var t = $form.attr('target'), a = $form.attr('action');
+
+ // update form attrs in IE friendly way
+ form.setAttribute('target',id);
+ if (!method) {
+ form.setAttribute('method', 'POST');
+ }
+ if (a != s.url) {
+ form.setAttribute('action', s.url);
+ }
+
+ // ie borks in some cases when setting encoding
+ if (! s.skipEncodingOverride && (!method || /post/i.test(method))) {
+ $form.attr({
+ encoding: 'multipart/form-data',
+ enctype: 'multipart/form-data'
+ });
+ }
+
+ // support timout
+ if (s.timeout) {
+ timeoutHandle = setTimeout(function() { timedOut = true; cb(CLIENT_TIMEOUT_ABORT); }, s.timeout);
+ }
+
+ // look for server aborts
+ function checkState() {
+ try {
+ var state = getDoc(io).readyState;
+ log('state = ' + state);
+ if (state && state.toLowerCase() == 'uninitialized')
+ setTimeout(checkState,50);
+ }
+ catch(e) {
+ log('Server abort: ' , e, ' (', e.name, ')');
+ cb(SERVER_ABORT);
+ if (timeoutHandle)
+ clearTimeout(timeoutHandle);
+ timeoutHandle = undefined;
+ }
+ }
+
+ // add "extra" data to form if provided in options
+ var extraInputs = [];
+ try {
+ if (s.extraData) {
+ for (var n in s.extraData) {
+ if (s.extraData.hasOwnProperty(n)) {
+ // if using the $.param format that allows for multiple values with the same name
+ if($.isPlainObject(s.extraData[n]) && s.extraData[n].hasOwnProperty('name') && s.extraData[n].hasOwnProperty('value')) {
+ extraInputs.push(
+ $('<input type="hidden" name="'+s.extraData[n].name+'">').attr('value',s.extraData[n].value)
+ .appendTo(form)[0]);
+ } else {
+ extraInputs.push(
+ $('<input type="hidden" name="'+n+'">').attr('value',s.extraData[n])
+ .appendTo(form)[0]);
+ }
+ }
+ }
+ }
+
+ if (!s.iframeTarget) {
+ // add iframe to doc and submit the form
+ $io.appendTo('body');
+ if (io.attachEvent)
+ io.attachEvent('onload', cb);
+ else
+ io.addEventListener('load', cb, false);
+ }
+ setTimeout(checkState,15);
+ form.submit();
+ }
+ finally {
+ // reset attrs and remove "extra" input elements
+ form.setAttribute('action',a);
+ if(t) {
+ form.setAttribute('target', t);
+ } else {
+ $form.removeAttr('target');
+ }
+ $(extraInputs).remove();
+ }
+ }
+
+ if (s.forceSync) {
+ doSubmit();
+ }
+ else {
+ setTimeout(doSubmit, 10); // this lets dom updates render
+ }
+
+ var data, doc, domCheckCount = 50, callbackProcessed;
+
+ function cb(e) {
+ if (xhr.aborted || callbackProcessed) {
+ return;
+ }
+ try {
+ doc = getDoc(io);
+ }
+ catch(ex) {
+ log('cannot access response document: ', ex);
+ e = SERVER_ABORT;
+ }
+ if (e === CLIENT_TIMEOUT_ABORT && xhr) {
+ xhr.abort('timeout');
+ return;
+ }
+ else if (e == SERVER_ABORT && xhr) {
+ xhr.abort('server abort');
+ return;
+ }
+
+ if (!doc || doc.location.href == s.iframeSrc) {
+ // response not received yet
+ if (!timedOut)
+ return;
+ }
+ if (io.detachEvent)
+ io.detachEvent('onload', cb);
+ else
+ io.removeEventListener('load', cb, false);
+
+ var status = 'success', errMsg;
+ try {
+ if (timedOut) {
+ throw 'timeout';
+ }
+
+ var isXml = s.dataType == 'xml' || doc.XMLDocument || $.isXMLDoc(doc);
+ log('isXml='+isXml);
+ if (!isXml && window.opera && (doc.body === null || !doc.body.innerHTML)) {
+ if (--domCheckCount) {
+ // in some browsers (Opera) the iframe DOM is not always traversable when
+ // the onload callback fires, so we loop a bit to accommodate
+ log('requeing onLoad callback, DOM not available');
+ setTimeout(cb, 250);
+ return;
+ }
+ // let this fall through because server response could be an empty document
+ //log('Could not access iframe DOM after mutiple tries.');
+ //throw 'DOMException: not available';
+ }
+
+ //log('response detected');
+ var docRoot = doc.body ? doc.body : doc.documentElement;
+ xhr.responseText = docRoot ? docRoot.innerHTML : null;
+ xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc;
+ if (isXml)
+ s.dataType = 'xml';
+ xhr.getResponseHeader = function(header){
+ var headers = {'content-type': s.dataType};
+ return headers[header];
+ };
+ // support for XHR 'status' & 'statusText' emulation :
+ if (docRoot) {
+ xhr.status = Number( docRoot.getAttribute('status') ) || xhr.status;
+ xhr.statusText = docRoot.getAttribute('statusText') || xhr.statusText;
+ }
+
+ var dt = (s.dataType || '').toLowerCase();
+ var scr = /(json|script|text)/.test(dt);
+ if (scr || s.textarea) {
+ // see if user embedded response in textarea
+ var ta = doc.getElementsByTagName('textarea')[0];
+ if (ta) {
+ xhr.responseText = ta.value;
+ // support for XHR 'status' & 'statusText' emulation :
+ xhr.status = Number( ta.getAttribute('status') ) || xhr.status;
+ xhr.statusText = ta.getAttribute('statusText') || xhr.statusText;
+ }
+ else if (scr) {
+ // account for browsers injecting pre around json response
+ var pre = doc.getElementsByTagName('pre')[0];
+ var b = doc.getElementsByTagName('body')[0];
+ if (pre) {
+ xhr.responseText = pre.textContent ? pre.textContent : pre.innerText;
+ }
+ else if (b) {
+ xhr.responseText = b.textContent ? b.textContent : b.innerText;
+ }
+ }
+ }
+ else if (dt == 'xml' && !xhr.responseXML && xhr.responseText) {
+ xhr.responseXML = toXml(xhr.responseText);
+ }
+
+ try {
+ data = httpData(xhr, dt, s);
+ }
+ catch (e) {
+ status = 'parsererror';
+ xhr.error = errMsg = (e || status);
+ }
+ }
+ catch (e) {
+ log('error caught: ',e);
+ status = 'error';
+ xhr.error = errMsg = (e || status);
+ }
+
+ if (xhr.aborted) {
+ log('upload aborted');
+ status = null;
+ }
+
+ if (xhr.status) { // we've set xhr.status
+ status = (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) ? 'success' : 'error';
+ }
+
+ // ordering of these callbacks/triggers is odd, but that's how $.ajax does it
+ if (status === 'success') {
+ if (s.success)
+ s.success.call(s.context, data, 'success', xhr);
+ if (g)
+ $.event.trigger("ajaxSuccess", [xhr, s]);
+ }
+ else if (status) {
+ if (errMsg === undefined)
+ errMsg = xhr.statusText;
+ if (s.error)
+ s.error.call(s.context, xhr, status, errMsg);
+ if (g)
+ $.event.trigger("ajaxError", [xhr, s, errMsg]);
+ }
+
+ if (g)
+ $.event.trigger("ajaxComplete", [xhr, s]);
+
+ if (g && ! --$.active) {
+ $.event.trigger("ajaxStop");
+ }
+
+ if (s.complete)
+ s.complete.call(s.context, xhr, status);
+
+ callbackProcessed = true;
+ if (s.timeout)
+ clearTimeout(timeoutHandle);
+
+ // clean up
+ setTimeout(function() {
+ if (!s.iframeTarget)
+ $io.remove();
+ xhr.responseXML = null;
+ }, 100);
+ }
+
+ var toXml = $.parseXML || function(s, doc) { // use parseXML if available (jQuery 1.5+)
+ if (window.ActiveXObject) {
+ doc = new ActiveXObject('Microsoft.XMLDOM');
+ doc.async = 'false';
+ doc.loadXML(s);
+ }
+ else {
+ doc = (new DOMParser()).parseFromString(s, 'text/xml');
+ }
+ return (doc && doc.documentElement && doc.documentElement.nodeName != 'parsererror') ? doc : null;
+ };
+ var parseJSON = $.parseJSON || function(s) {
+ /*jslint evil:true */
+ return window['eval']('(' + s + ')');
+ };
+
+ var httpData = function( xhr, type, s ) { // mostly lifted from jq1.4.4
+
+ var ct = xhr.getResponseHeader('content-type') || '',
+ xml = type === 'xml' || !type && ct.indexOf('xml') >= 0,
+ data = xml ? xhr.responseXML : xhr.responseText;
+
+ if (xml && data.documentElement.nodeName === 'parsererror') {
+ if ($.error)
+ $.error('parsererror');
+ }
+ if (s && s.dataFilter) {
+ data = s.dataFilter(data, type);
+ }
+ if (typeof data === 'string') {
+ if (type === 'json' || !type && ct.indexOf('json') >= 0) {
+ data = parseJSON(data);
+ } else if (type === "script" || !type && ct.indexOf("javascript") >= 0) {
+ $.globalEval(data);
+ }
+ }
+ return data;
+ };
+ }
+};
+
+/**
+ * ajaxForm() provides a mechanism for fully automating form submission.
+ *
+ * The advantages of using this method instead of ajaxSubmit() are:
+ *
+ * 1: This method will include coordinates for <input type="image" /> elements (if the element
+ * is used to submit the form).
+ * 2. This method will include the submit element's name/value data (for the element that was
+ * used to submit the form).
+ * 3. This method binds the submit() method to the form for you.
+ *
+ * The options argument for ajaxForm works exactly as it does for ajaxSubmit. ajaxForm merely
+ * passes the options argument along after properly binding events for submit elements and
+ * the form itself.
+ */
+$.fn.ajaxForm = function(options) {
+ options = options || {};
+ options.delegation = options.delegation && $.isFunction($.fn.on);
+
+ // in jQuery 1.3+ we can fix mistakes with the ready state
+ if (!options.delegation && this.length === 0) {
+ var o = { s: this.selector, c: this.context };
+ if (!$.isReady && o.s) {
+ log('DOM not ready, queuing ajaxForm');
+ $(function() {
+ $(o.s,o.c).ajaxForm(options);
+ });
+ return this;
+ }
+ // is your DOM ready? http://docs.jquery.com/Tutorials:Introducing_$(document).ready()
+ log('terminating; zero elements found by selector' + ($.isReady ? '' : ' (DOM not ready)'));
+ return this;
+ }
+
+ if ( options.delegation ) {
+ $(document)
+ .off('submit.form-plugin', this.selector, doAjaxSubmit)
+ .off('click.form-plugin', this.selector, captureSubmittingElement)
+ .on('submit.form-plugin', this.selector, options, doAjaxSubmit)
+ .on('click.form-plugin', this.selector, options, captureSubmittingElement);
+ return this;
+ }
+
+ return this.ajaxFormUnbind()
+ .bind('submit.form-plugin', options, doAjaxSubmit)
+ .bind('click.form-plugin', options, captureSubmittingElement);
+};
+
+// private event handlers
+function doAjaxSubmit(e) {
+ /*jshint validthis:true */
+ var options = e.data;
+ if (!e.isDefaultPrevented()) { // if event has been canceled, don't proceed
+ e.preventDefault();
+ $(this).ajaxSubmit(options);
+ }
+}
+
+function captureSubmittingElement(e) {
+ /*jshint validthis:true */
+ var target = e.target;
+ var $el = $(target);
+ if (!($el.is(":submit,input:image"))) {
+ // is this a child element of the submit el? (ex: a span within a button)
+ var t = $el.closest(':submit');
+ if (t.length === 0) {
+ return;
+ }
+ target = t[0];
+ }
+ var form = this;
+ form.clk = target;
+ if (target.type == 'image') {
+ if (e.offsetX !== undefined) {
+ form.clk_x = e.offsetX;
+ form.clk_y = e.offsetY;
+ } else if (typeof $.fn.offset == 'function') {
+ var offset = $el.offset();
+ form.clk_x = e.pageX - offset.left;
+ form.clk_y = e.pageY - offset.top;
+ } else {
+ form.clk_x = e.pageX - target.offsetLeft;
+ form.clk_y = e.pageY - target.offsetTop;
+ }
+ }
+ // clear form vars
+ setTimeout(function() { form.clk = form.clk_x = form.clk_y = null; }, 100);
+}
+
+
+// ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm
+$.fn.ajaxFormUnbind = function() {
+ return this.unbind('submit.form-plugin click.form-plugin');
+};
+
+/**
+ * formToArray() gathers form element data into an array of objects that can
+ * be passed to any of the following ajax functions: $.get, $.post, or load.
+ * Each object in the array has both a 'name' and 'value' property. An example of
+ * an array for a simple login form might be:
+ *
+ * [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ]
+ *
+ * It is this array that is passed to pre-submit callback functions provided to the
+ * ajaxSubmit() and ajaxForm() methods.
+ */
+$.fn.formToArray = function(semantic, elements) {
+ var a = [];
+ if (this.length === 0) {
+ return a;
+ }
+
+ var form = this[0];
+ var els = semantic ? form.getElementsByTagName('*') : form.elements;
+ if (!els) {
+ return a;
+ }
+
+ var i,j,n,v,el,max,jmax;
+ for(i=0, max=els.length; i < max; i++) {
+ el = els[i];
+ n = el.name;
+ if (!n) {
+ continue;
+ }
+
+ if (semantic && form.clk && el.type == "image") {
+ // handle image inputs on the fly when semantic == true
+ if(!el.disabled && form.clk == el) {
+ a.push({name: n, value: $(el).val(), type: el.type });
+ a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
+ }
+ continue;
+ }
+
+ v = $.fieldValue(el, true);
+ if (v && v.constructor == Array) {
+ if (elements)
+ elements.push(el);
+ for(j=0, jmax=v.length; j < jmax; j++) {
+ a.push({name: n, value: v[j]});
+ }
+ }
+ else if (feature.fileapi && el.type == 'file' && !el.disabled) {
+ if (elements)
+ elements.push(el);
+ var files = el.files;
+ if (files.length) {
+ for (j=0; j < files.length; j++) {
+ a.push({name: n, value: files[j], type: el.type});
+ }
+ }
+ else {
+ // #180
+ a.push({ name: n, value: '', type: el.type });
+ }
+ }
+ else if (v !== null && typeof v != 'undefined') {
+ if (elements)
+ elements.push(el);
+ a.push({name: n, value: v, type: el.type, required: el.required});
+ }
+ }
+
+ if (!semantic && form.clk) {
+ // input type=='image' are not found in elements array! handle it here
+ var $input = $(form.clk), input = $input[0];
+ n = input.name;
+ if (n && !input.disabled && input.type == 'image') {
+ a.push({name: n, value: $input.val()});
+ a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
+ }
+ }
+ return a;
+};
+
+/**
+ * Serializes form data into a 'submittable' string. This method will return a string
+ * in the format: name1=value1&name2=value2
+ */
+$.fn.formSerialize = function(semantic) {
+ //hand off to jQuery.param for proper encoding
+ return $.param(this.formToArray(semantic));
+};
+
+/**
+ * Serializes all field elements in the jQuery object into a query string.
+ * This method will return a string in the format: name1=value1&name2=value2
+ */
+$.fn.fieldSerialize = function(successful) {
+ var a = [];
+ this.each(function() {
+ var n = this.name;
+ if (!n) {
+ return;
+ }
+ var v = $.fieldValue(this, successful);
+ if (v && v.constructor == Array) {
+ for (var i=0,max=v.length; i < max; i++) {
+ a.push({name: n, value: v[i]});
+ }
+ }
+ else if (v !== null && typeof v != 'undefined') {
+ a.push({name: this.name, value: v});
+ }
+ });
+ //hand off to jQuery.param for proper encoding
+ return $.param(a);
+};
+
+/**
+ * Returns the value(s) of the element in the matched set. For example, consider the following form:
+ *
+ * <form><fieldset>
+ * <input name="A" type="text" />
+ * <input name="A" type="text" />
+ * <input name="B" type="checkbox" value="B1" />
+ * <input name="B" type="checkbox" value="B2"/>
+ * <input name="C" type="radio" value="C1" />
+ * <input name="C" type="radio" value="C2" />
+ * </fieldset></form>
+ *
+ * var v = $(':text').fieldValue();
+ * // if no values are entered into the text inputs
+ * v == ['','']
+ * // if values entered into the text inputs are 'foo' and 'bar'
+ * v == ['foo','bar']
+ *
+ * var v = $(':checkbox').fieldValue();
+ * // if neither checkbox is checked
+ * v === undefined
+ * // if both checkboxes are checked
+ * v == ['B1', 'B2']
+ *
+ * var v = $(':radio').fieldValue();
+ * // if neither radio is checked
+ * v === undefined
+ * // if first radio is checked
+ * v == ['C1']
+ *
+ * The successful argument controls whether or not the field element must be 'successful'
+ * (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls).
+ * The default value of the successful argument is true. If this value is false the value(s)
+ * for each element is returned.
+ *
+ * Note: This method *always* returns an array. If no valid value can be determined the
+ * array will be empty, otherwise it will contain one or more values.
+ */
+$.fn.fieldValue = function(successful) {
+ for (var val=[], i=0, max=this.length; i < max; i++) {
+ var el = this[i];
+ var v = $.fieldValue(el, successful);
+ if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length)) {
+ continue;
+ }
+ if (v.constructor == Array)
+ $.merge(val, v);
+ else
+ val.push(v);
+ }
+ return val;
+};
+
+/**
+ * Returns the value of the field element.
+ */
+$.fieldValue = function(el, successful) {
+ var n = el.name, t = el.type, tag = el.tagName.toLowerCase();
+ if (successful === undefined) {
+ successful = true;
+ }
+
+ if (successful && (!n || el.disabled || t == 'reset' || t == 'button' ||
+ (t == 'checkbox' || t == 'radio') && !el.checked ||
+ (t == 'submit' || t == 'image') && el.form && el.form.clk != el ||
+ tag == 'select' && el.selectedIndex == -1)) {
+ return null;
+ }
+
+ if (tag == 'select') {
+ var index = el.selectedIndex;
+ if (index < 0) {
+ return null;
+ }
+ var a = [], ops = el.options;
+ var one = (t == 'select-one');
+ var max = (one ? index+1 : ops.length);
+ for(var i=(one ? index : 0); i < max; i++) {
+ var op = ops[i];
+ if (op.selected) {
+ var v = op.value;
+ if (!v) { // extra pain for IE...
+ v = (op.attributes && op.attributes['value'] && !(op.attributes['value'].specified)) ? op.text : op.value;
+ }
+ if (one) {
+ return v;
+ }
+ a.push(v);
+ }
+ }
+ return a;
+ }
+ return $(el).val();
+};
+
+/**
+ * Clears the form data. Takes the following actions on the form's input fields:
+ * - input text fields will have their 'value' property set to the empty string
+ * - select elements will have their 'selectedIndex' property set to -1
+ * - checkbox and radio inputs will have their 'checked' property set to false
+ * - inputs of type submit, button, reset, and hidden will *not* be effected
+ * - button elements will *not* be effected
+ */
+$.fn.clearForm = function(includeHidden) {
+ return this.each(function() {
+ $('input,select,textarea', this).clearFields(includeHidden);
+ });
+};
+
+/**
+ * Clears the selected form elements.
+ */
+$.fn.clearFields = $.fn.clearInputs = function(includeHidden) {
+ var re = /^(?:color|date|datetime|email|month|number|password|range|search|tel|text|time|url|week)$/i; // 'hidden' is not in this list
+ return this.each(function() {
+ var t = this.type, tag = this.tagName.toLowerCase();
+ if (re.test(t) || tag == 'textarea') {
+ this.value = '';
+ }
+ else if (t == 'checkbox' || t == 'radio') {
+ this.checked = false;
+ }
+ else if (tag == 'select') {
+ this.selectedIndex = -1;
+ }
+ else if (includeHidden) {
+ // includeHidden can be the value true, or it can be a selector string
+ // indicating a special test; for example:
+ // $('#myForm').clearForm('.special:hidden')
+ // the above would clean hidden inputs that have the class of 'special'
+ if ( (includeHidden === true && /hidden/.test(t)) ||
+ (typeof includeHidden == 'string' && $(this).is(includeHidden)) )
+ this.value = '';
+ }
+ });
+};
+
+/**
+ * Resets the form data. Causes all form elements to be reset to their original value.
+ */
+$.fn.resetForm = function() {
+ return this.each(function() {
+ // guard against an input with the name of 'reset'
+ // note that IE reports the reset function as an 'object'
+ if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType)) {
+ this.reset();
+ }
+ });
+};
+
+/**
+ * Enables or disables any matching elements.
+ */
+$.fn.enable = function(b) {
+ if (b === undefined) {
+ b = true;
+ }
+ return this.each(function() {
+ this.disabled = !b;
+ });
+};
+
+/**
+ * Checks/unchecks any matching checkboxes or radio buttons and
+ * selects/deselects and matching option elements.
+ */
+$.fn.selected = function(select) {
+ if (select === undefined) {
+ select = true;
+ }
+ return this.each(function() {
+ var t = this.type;
+ if (t == 'checkbox' || t == 'radio') {
+ this.checked = select;
+ }
+ else if (this.tagName.toLowerCase() == 'option') {
+ var $sel = $(this).parent('select');
+ if (select && $sel[0] && $sel[0].type == 'select-one') {
+ // deselect all other options
+ $sel.find('option').selected(false);
+ }
+ this.selected = select;
+ }
+ });
+};
+
+// expose debug var
+$.fn.ajaxSubmit.debug = false;
+
+// helper fn for console logging
+function log() {
+ if (!$.fn.ajaxSubmit.debug)
+ return;
+ var msg = '[jquery.form] ' + Array.prototype.join.call(arguments,'');
+ if (window.console && window.console.log) {
+ window.console.log(msg);
+ }
+ else if (window.opera && window.opera.postError) {
+ window.opera.postError(msg);
+ }
+}
+
+})(jQuery);
+++ /dev/null
-/**
- * jQuery fullscreen plugin v2.0.0-git (9f8f97d127)
- * https://github.com/theopolisme/jquery-fullscreen
- *
- * Copyright (c) 2013 Theopolisme <theopolismewiki@gmail.com>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 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 General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- */
-( function ( $ ) {
- var setupFullscreen,
- fsClass = 'jq-fullscreened';
-
- /**
- * On fullscreenchange, trigger a jq-fullscreen-change event
- * The event is given an object, which contains the fullscreened DOM element (element), if any
- * and a boolean value (fullscreen) indicating if we've entered or exited fullscreen mode
- * Also remove the 'fullscreened' class from elements that are no longer fullscreen
- */
- function handleFullscreenChange () {
- var fullscreenElement = document.fullscreenElement ||
- document.mozFullScreenElement ||
- document.webkitFullscreenElement ||
- document.msFullscreenElement;
-
- if ( !fullscreenElement ) {
- $( '.' + fsClass ).data( 'isFullscreened', false ).removeClass( fsClass );
- }
-
- $( document ).trigger( $.Event( 'jq-fullscreen-change', { element: fullscreenElement, fullscreen: !!fullscreenElement } ) );
- }
-
- /**
- * Enters full screen with the "this" element in focus.
- * Check the .data( 'isFullscreened' ) of the return value to check
- * success or failure, if you're into that sort of thing.
- * @chainable
- * @return {jQuery}
- */
- function enterFullscreen () {
- var element = this.get(0),
- $element = this.first();
- if ( element ) {
- if ( element.requestFullscreen ) {
- element.requestFullscreen();
- } else if ( element.mozRequestFullScreen ) {
- element.mozRequestFullScreen();
- } else if ( element.webkitRequestFullscreen ) {
- element.webkitRequestFullscreen();
- } else if ( element.msRequestFullscreen ) {
- element.msRequestFullscreen();
- } else {
- // Unable to make fullscreen
- $element.data( 'isFullscreened', false );
- return this;
- }
- // Add the fullscreen class and data attribute to `element`
- $element.addClass( fsClass ).data( 'isFullscreened', true );
- return this;
- } else {
- $element.data( 'isFullscreened', false );
- return this;
- }
- }
-
- /**
- * Brings the "this" element out of fullscreen.
- * Check the .data( 'isFullscreened' ) of the return value to check
- * success or failure, if you're into that sort of thing.
- * @chainable
- * @return {jQuery}
- */
- function exitFullscreen () {
- var fullscreenElement = ( document.fullscreenElement ||
- document.mozFullScreenElement ||
- document.webkitFullscreenElement ||
- document.msFullscreenElement );
-
- // Ensure that we only exit fullscreen if exitFullscreen() is being called on the same element that is currently fullscreen
- if ( fullscreenElement && this.get(0) === fullscreenElement ) {
- if ( document.exitFullscreen ) {
- document.exitFullscreen();
- } else if ( document.mozCancelFullScreen ) {
- document.mozCancelFullScreen();
- } else if ( document.webkitCancelFullScreen ) {
- document.webkitCancelFullScreen();
- } else if ( document.msExitFullscreen ) {
- document.msExitFullscreen();
- } else {
- // Unable to cancel fullscreen mode
- return this;
- }
- // We don't need to remove the fullscreen class here,
- // because it will be removed in handleFullscreenChange.
- // But we should change the data on the element so the
- // caller can check for success.
- this.first().data( 'isFullscreened', false );
- }
-
- return this;
- }
-
- /**
- * Set up fullscreen handling and install necessary event handlers.
- * Return false if fullscreen is not supported.
- */
- setupFullscreen = function () {
- if ( $.support.fullscreen ) {
- // When the fullscreen mode is changed, trigger the
- // fullscreen events (and when exiting,
- // also remove the fullscreen class)
- $( document ).on( 'fullscreenchange webkitfullscreenchange mozfullscreenchange MSFullscreenChange', handleFullscreenChange);
- // Convenience wrapper so that one only needs to listen for
- // 'fullscreenerror', not all of the prefixed versions
- $( document ).on( 'webkitfullscreenerror mozfullscreenerror MSFullscreenError', function () {
- $( document ).trigger( $.Event( 'fullscreenerror' ) );
- } );
- // Fullscreen has been set up, so always return true
- setupFullscreen = function () { return true; };
- return true;
- } else {
- // Always return false from now on, since fullscreen is not supported
- setupFullscreen = function () { return false; };
- return false;
- }
- };
-
- /**
- * Set up fullscreen handling if necessary, then make the first element
- * matching the given selector fullscreen
- * @chainable
- * @return {jQuery}
- */
- $.fn.enterFullscreen = function () {
- if ( setupFullscreen() ) {
- $.fn.enterFullscreen = enterFullscreen;
- return this.enterFullscreen();
- } else {
- $.fn.enterFullscreen = function () { return this; };
- return this;
- }
- };
-
- /**
- * Set up fullscreen handling if necessary, then cancel fullscreen mode
- * for the first element matching the given selector.
- * @chainable
- * @return {jQuery}
- */
- $.fn.exitFullscreen = function () {
- if ( setupFullscreen() ) {
- $.fn.exitFullscreen = exitFullscreen;
- return this.exitFullscreen();
- } else {
- $.fn.exitFullscreen = function () { return this; };
- return this;
- }
- };
-
- $.support.fullscreen = document.fullscreenEnabled ||
- document.webkitFullscreenEnabled ||
- document.mozFullScreenEnabled ||
- document.msFullscreenEnabled;
-}( jQuery ) );
--- /dev/null
+/**
+ * jQuery fullscreen plugin
+ * https://github.com/theopolisme/jquery-fullscreen
+ *
+ * Copyright (c) 2013 Theopolisme <theopolismewiki@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+( function ( $ ) {
+ var setupFullscreen,
+ fsClass = 'jq-fullscreened';
+
+ /**
+ * On fullscreenchange, trigger a jq-fullscreen-change event
+ * The event is given an object, which contains the fullscreened DOM element (element), if any
+ * and a boolean value (fullscreen) indicating if we've entered or exited fullscreen mode
+ * Also remove the 'fullscreened' class from elements that are no longer fullscreen
+ */
+ function handleFullscreenChange () {
+ var fullscreenElement = document.fullscreenElement ||
+ document.mozFullScreenElement ||
+ document.webkitFullscreenElement ||
+ document.msFullscreenElement;
+
+ if ( !fullscreenElement ) {
+ $( '.' + fsClass ).data( 'isFullscreened', false ).removeClass( fsClass );
+ }
+
+ $( document ).trigger( $.Event( 'jq-fullscreen-change', { element: fullscreenElement, fullscreen: !!fullscreenElement } ) );
+ }
+
+ /**
+ * Enters full screen with the "this" element in focus.
+ * Check the .data( 'isFullscreened' ) of the return value to check
+ * success or failure, if you're into that sort of thing.
+ * @chainable
+ * @return {jQuery}
+ */
+ function enterFullscreen () {
+ var element = this.get(0),
+ $element = this.first();
+ if ( element ) {
+ if ( element.requestFullscreen ) {
+ element.requestFullscreen();
+ } else if ( element.mozRequestFullScreen ) {
+ element.mozRequestFullScreen();
+ } else if ( element.webkitRequestFullscreen ) {
+ element.webkitRequestFullscreen();
+ } else if ( element.msRequestFullscreen ) {
+ element.msRequestFullscreen();
+ } else {
+ // Unable to make fullscreen
+ $element.data( 'isFullscreened', false );
+ return this;
+ }
+ // Add the fullscreen class and data attribute to `element`
+ $element.addClass( fsClass ).data( 'isFullscreened', true );
+ return this;
+ } else {
+ $element.data( 'isFullscreened', false );
+ return this;
+ }
+ }
+
+ /**
+ * Brings the "this" element out of fullscreen.
+ * Check the .data( 'isFullscreened' ) of the return value to check
+ * success or failure, if you're into that sort of thing.
+ * @chainable
+ * @return {jQuery}
+ */
+ function exitFullscreen () {
+ var fullscreenElement = ( document.fullscreenElement ||
+ document.mozFullScreenElement ||
+ document.webkitFullscreenElement ||
+ document.msFullscreenElement );
+
+ // Ensure that we only exit fullscreen if exitFullscreen() is being called on the same element that is currently fullscreen
+ if ( fullscreenElement && this.get(0) === fullscreenElement ) {
+ if ( document.exitFullscreen ) {
+ document.exitFullscreen();
+ } else if ( document.mozCancelFullScreen ) {
+ document.mozCancelFullScreen();
+ } else if ( document.webkitCancelFullScreen ) {
+ document.webkitCancelFullScreen();
+ } else if ( document.msExitFullscreen ) {
+ document.msExitFullscreen();
+ } else {
+ // Unable to cancel fullscreen mode
+ return this;
+ }
+ // We don't need to remove the fullscreen class here,
+ // because it will be removed in handleFullscreenChange.
+ // But we should change the data on the element so the
+ // caller can check for success.
+ this.first().data( 'isFullscreened', false );
+ }
+
+ return this;
+ }
+
+ /**
+ * Set up fullscreen handling and install necessary event handlers.
+ * Return false if fullscreen is not supported.
+ */
+ setupFullscreen = function () {
+ if ( $.support.fullscreen ) {
+ // When the fullscreen mode is changed, trigger the
+ // fullscreen events (and when exiting,
+ // also remove the fullscreen class)
+ $( document ).on( 'fullscreenchange webkitfullscreenchange mozfullscreenchange MSFullscreenChange', handleFullscreenChange);
+ // Convenience wrapper so that one only needs to listen for
+ // 'fullscreenerror', not all of the prefixed versions
+ $( document ).on( 'webkitfullscreenerror mozfullscreenerror MSFullscreenError', function () {
+ $( document ).trigger( $.Event( 'fullscreenerror' ) );
+ } );
+ // Fullscreen has been set up, so always return true
+ setupFullscreen = function () { return true; };
+ return true;
+ } else {
+ // Always return false from now on, since fullscreen is not supported
+ setupFullscreen = function () { return false; };
+ return false;
+ }
+ };
+
+ /**
+ * Set up fullscreen handling if necessary, then make the first element
+ * matching the given selector fullscreen
+ * @chainable
+ * @return {jQuery}
+ */
+ $.fn.enterFullscreen = function () {
+ if ( setupFullscreen() ) {
+ $.fn.enterFullscreen = enterFullscreen;
+ return this.enterFullscreen();
+ } else {
+ $.fn.enterFullscreen = function () { return this; };
+ return this;
+ }
+ };
+
+ /**
+ * Set up fullscreen handling if necessary, then cancel fullscreen mode
+ * for the first element matching the given selector.
+ * @chainable
+ * @return {jQuery}
+ */
+ $.fn.exitFullscreen = function () {
+ if ( setupFullscreen() ) {
+ $.fn.exitFullscreen = exitFullscreen;
+ return this.exitFullscreen();
+ } else {
+ $.fn.exitFullscreen = function () { return this; };
+ return this;
+ }
+ };
+
+ $.support.fullscreen = document.fullscreenEnabled ||
+ document.webkitFullscreenEnabled ||
+ document.mozFullScreenEnabled ||
+ document.msFullscreenEnabled;
+}( jQuery ) );
+++ /dev/null
-/**
-* hoverIntent is similar to jQuery's built-in "hover" function except that
-* instead of firing the onMouseOver event immediately, hoverIntent checks
-* to see if the user's mouse has slowed down (beneath the sensitivity
-* threshold) before firing the onMouseOver event.
-*
-* hoverIntent r5 // 2007.03.27 // jQuery 1.1.2+
-* <http://cherne.net/brian/resources/jquery.hoverIntent.html>
-*
-* hoverIntent is currently available for use in all personal or commercial
-* projects under both MIT and GPL licenses. This means that you can choose
-* the license that best suits your project, and use it accordingly.
-*
-* // basic usage (just like .hover) receives onMouseOver and onMouseOut functions
-* $("ul li").hoverIntent( showNav , hideNav );
-*
-* // advanced usage receives configuration object only
-* $("ul li").hoverIntent({
-* sensitivity: 7, // number = sensitivity threshold (must be 1 or higher)
-* interval: 100, // number = milliseconds of polling interval
-* over: showNav, // function = onMouseOver callback (required)
-* timeout: 0, // number = milliseconds delay before onMouseOut function call
-* out: hideNav // function = onMouseOut callback (required)
-* });
-*
-* @param f onMouseOver function || An object with configuration options
-* @param g onMouseOut function || Nothing (use configuration options object)
-* @author Brian Cherne <brian@cherne.net>
-*/
-(function($) {
- $.fn.hoverIntent = function(f,g) {
- // default configuration options
- var cfg = {
- sensitivity: 7,
- interval: 100,
- timeout: 0
- };
- // override configuration options with user supplied object
- cfg = $.extend(cfg, g ? { over: f, out: g } : f );
-
- // instantiate variables
- // cX, cY = current X and Y position of mouse, updated by mousemove event
- // pX, pY = previous X and Y position of mouse, set by mouseover and polling interval
- var cX, cY, pX, pY;
-
- // A private function for getting mouse position
- var track = function(ev) {
- cX = ev.pageX;
- cY = ev.pageY;
- };
-
- // A private function for comparing current and previous mouse position
- var compare = function(ev,ob) {
- ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t);
- // compare mouse positions to see if they've crossed the threshold
- if ( ( Math.abs(pX-cX) + Math.abs(pY-cY) ) < cfg.sensitivity ) {
- $(ob).unbind("mousemove",track);
- // set hoverIntent state to true (so mouseOut can be called)
- ob.hoverIntent_s = 1;
- return cfg.over.apply(ob,[ev]);
- } else {
- // set previous coordinates for next time
- pX = cX; pY = cY;
- // use self-calling timeout, guarantees intervals are spaced out properly (avoids JavaScript timer bugs)
- ob.hoverIntent_t = setTimeout( function(){compare(ev, ob);} , cfg.interval );
- }
- };
-
- // A private function for delaying the mouseOut function
- var delay = function(ev,ob) {
- ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t);
- ob.hoverIntent_s = 0;
- return cfg.out.apply(ob,[ev]);
- };
-
- // A private function for handling mouse 'hovering'
- var handleHover = function(e) {
- // next three lines copied from jQuery.hover, ignore children onMouseOver/onMouseOut
- var p = (e.type == "mouseover" ? e.fromElement : e.toElement) || e.relatedTarget;
- while ( p && p != this ) { try { p = p.parentNode; } catch(e) { p = this; } }
- if ( p == this ) { return false; }
-
- // copy objects to be passed into t (required for event object to be passed in IE)
- var ev = $.extend({},e);
- var ob = this;
-
- // cancel hoverIntent timer if it exists
- if (ob.hoverIntent_t) { ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t); }
-
- // else e.type == "onmouseover"
- if (e.type == "mouseover") {
- // set "previous" X and Y position based on initial entry point
- pX = ev.pageX; pY = ev.pageY;
- // update "current" X and Y position based on mousemove
- $(ob).bind("mousemove",track);
- // start polling interval (self-calling timeout) to compare mouse coordinates over time
- if (ob.hoverIntent_s != 1) { ob.hoverIntent_t = setTimeout( function(){compare(ev,ob);} , cfg.interval );}
-
- // else e.type == "onmouseout"
- } else {
- // unbind expensive mousemove event
- $(ob).unbind("mousemove",track);
- // if hoverIntent state is true, then call the mouseOut function after the specified delay
- if (ob.hoverIntent_s == 1) { ob.hoverIntent_t = setTimeout( function(){delay(ev,ob);} , cfg.timeout );}
- }
- };
-
- // bind the function to the two event listeners
- return this.mouseover(handleHover).mouseout(handleHover);
- };
-})(jQuery);
\ No newline at end of file
--- /dev/null
+/**
+* hoverIntent is similar to jQuery's built-in "hover" function except that
+* instead of firing the onMouseOver event immediately, hoverIntent checks
+* to see if the user's mouse has slowed down (beneath the sensitivity
+* threshold) before firing the onMouseOver event.
+*
+* hoverIntent r5 // 2007.03.27 // jQuery 1.1.2+
+* <http://cherne.net/brian/resources/jquery.hoverIntent.html>
+*
+* hoverIntent is currently available for use in all personal or commercial
+* projects under both MIT and GPL licenses. This means that you can choose
+* the license that best suits your project, and use it accordingly.
+*
+* // basic usage (just like .hover) receives onMouseOver and onMouseOut functions
+* $("ul li").hoverIntent( showNav , hideNav );
+*
+* // advanced usage receives configuration object only
+* $("ul li").hoverIntent({
+* sensitivity: 7, // number = sensitivity threshold (must be 1 or higher)
+* interval: 100, // number = milliseconds of polling interval
+* over: showNav, // function = onMouseOver callback (required)
+* timeout: 0, // number = milliseconds delay before onMouseOut function call
+* out: hideNav // function = onMouseOut callback (required)
+* });
+*
+* @param f onMouseOver function || An object with configuration options
+* @param g onMouseOut function || Nothing (use configuration options object)
+* @author Brian Cherne <brian@cherne.net>
+*/
+(function($) {
+ $.fn.hoverIntent = function(f,g) {
+ // default configuration options
+ var cfg = {
+ sensitivity: 7,
+ interval: 100,
+ timeout: 0
+ };
+ // override configuration options with user supplied object
+ cfg = $.extend(cfg, g ? { over: f, out: g } : f );
+
+ // instantiate variables
+ // cX, cY = current X and Y position of mouse, updated by mousemove event
+ // pX, pY = previous X and Y position of mouse, set by mouseover and polling interval
+ var cX, cY, pX, pY;
+
+ // A private function for getting mouse position
+ var track = function(ev) {
+ cX = ev.pageX;
+ cY = ev.pageY;
+ };
+
+ // A private function for comparing current and previous mouse position
+ var compare = function(ev,ob) {
+ ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t);
+ // compare mouse positions to see if they've crossed the threshold
+ if ( ( Math.abs(pX-cX) + Math.abs(pY-cY) ) < cfg.sensitivity ) {
+ $(ob).unbind("mousemove",track);
+ // set hoverIntent state to true (so mouseOut can be called)
+ ob.hoverIntent_s = 1;
+ return cfg.over.apply(ob,[ev]);
+ } else {
+ // set previous coordinates for next time
+ pX = cX; pY = cY;
+ // use self-calling timeout, guarantees intervals are spaced out properly (avoids JavaScript timer bugs)
+ ob.hoverIntent_t = setTimeout( function(){compare(ev, ob);} , cfg.interval );
+ }
+ };
+
+ // A private function for delaying the mouseOut function
+ var delay = function(ev,ob) {
+ ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t);
+ ob.hoverIntent_s = 0;
+ return cfg.out.apply(ob,[ev]);
+ };
+
+ // A private function for handling mouse 'hovering'
+ var handleHover = function(e) {
+ // next three lines copied from jQuery.hover, ignore children onMouseOver/onMouseOut
+ var p = (e.type == "mouseover" ? e.fromElement : e.toElement) || e.relatedTarget;
+ while ( p && p != this ) { try { p = p.parentNode; } catch(e) { p = this; } }
+ if ( p == this ) { return false; }
+
+ // copy objects to be passed into t (required for event object to be passed in IE)
+ var ev = jQuery.extend({},e);
+ var ob = this;
+
+ // cancel hoverIntent timer if it exists
+ if (ob.hoverIntent_t) { ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t); }
+
+ // else e.type == "onmouseover"
+ if (e.type == "mouseover") {
+ // set "previous" X and Y position based on initial entry point
+ pX = ev.pageX; pY = ev.pageY;
+ // update "current" X and Y position based on mousemove
+ $(ob).bind("mousemove",track);
+ // start polling interval (self-calling timeout) to compare mouse coordinates over time
+ if (ob.hoverIntent_s != 1) { ob.hoverIntent_t = setTimeout( function(){compare(ev,ob);} , cfg.interval );}
+
+ // else e.type == "onmouseout"
+ } else {
+ // unbind expensive mousemove event
+ $(ob).unbind("mousemove",track);
+ // if hoverIntent state is true, then call the mouseOut function after the specified delay
+ if (ob.hoverIntent_s == 1) { ob.hoverIntent_t = setTimeout( function(){delay(ev,ob);} , cfg.timeout );}
+ }
+ };
+
+ // bind the function to the two event listeners
+ return this.mouseover(handleHover).mouseout(handleHover);
+ };
+})(jQuery);
\ No newline at end of file
+++ /dev/null
-/*
- * ----------------------------- JSTORAGE -------------------------------------
- * Simple local storage wrapper to save data on the browser side, supporting
- * all major browsers - IE6+, Firefox2+, Safari4+, Chrome4+ and Opera 10.5+
- *
- * Author: Andris Reinman, andris.reinman@gmail.com
- * Project homepage: www.jstorage.info
- *
- * Licensed under Unlicense:
- *
- * This is free and unencumbered software released into the public domain.
- *
- * Anyone is free to copy, modify, publish, use, compile, sell, or
- * distribute this software, either in source code form or as a compiled
- * binary, for any purpose, commercial or non-commercial, and by any
- * means.
- *
- * In jurisdictions that recognize copyright laws, the author or authors
- * of this software dedicate any and all copyright interest in the
- * software to the public domain. We make this dedication for the benefit
- * of the public at large and to the detriment of our heirs and
- * successors. We intend this dedication to be an overt act of
- * relinquishment in perpetuity of all present and future rights to this
- * software under copyright law.
- *
- * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
- * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
- * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
- * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
- * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
- * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
- * OTHER DEALINGS IN THE SOFTWARE.
- *
- * For more information, please refer to <http://unlicense.org/>
- */
-
-/* global ActiveXObject: false */
-/* jshint browser: true */
-
-(function() {
- 'use strict';
-
- var
- /* jStorage version */
- JSTORAGE_VERSION = '0.4.12',
-
- /* detect a dollar object or create one if not found */
- $ = window.jQuery || window.$ || (window.$ = {}),
-
- /* check for a JSON handling support */
- JSON = {
- parse: window.JSON && (window.JSON.parse || window.JSON.decode) ||
- String.prototype.evalJSON && function(str) {
- return String(str).evalJSON();
- } ||
- $.parseJSON ||
- $.evalJSON,
- stringify: Object.toJSON ||
- window.JSON && (window.JSON.stringify || window.JSON.encode) ||
- $.toJSON
- };
-
- // Break if no JSON support was found
- if (typeof JSON.parse !== 'function' || typeof JSON.stringify !== 'function') {
- throw new Error('No JSON support found, include //cdnjs.cloudflare.com/ajax/libs/json2/20110223/json2.js to page');
- }
-
- var
- /* This is the object, that holds the cached values */
- _storage = {
- __jstorage_meta: {
- CRC32: {}
- }
- },
-
- /* Actual browser storage (localStorage or globalStorage['domain']) */
- _storage_service = {
- jStorage: '{}'
- },
-
- /* DOM element for older IE versions, holds userData behavior */
- _storage_elm = null,
-
- /* How much space does the storage take */
- _storage_size = 0,
-
- /* which backend is currently used */
- _backend = false,
-
- /* onchange observers */
- _observers = {},
-
- /* timeout to wait after onchange event */
- _observer_timeout = false,
-
- /* last update time */
- _observer_update = 0,
-
- /* pubsub observers */
- _pubsub_observers = {},
-
- /* skip published items older than current timestamp */
- _pubsub_last = +new Date(),
-
- /* Next check for TTL */
- _ttl_timeout,
-
- /**
- * XML encoding and decoding as XML nodes can't be JSON'ized
- * XML nodes are encoded and decoded if the node is the value to be saved
- * but not if it's as a property of another object
- * Eg. -
- * $.jStorage.set('key', xmlNode); // IS OK
- * $.jStorage.set('key', {xml: xmlNode}); // NOT OK
- */
- _XMLService = {
-
- /**
- * Validates a XML node to be XML
- * based on jQuery.isXML function
- */
- isXML: function(elm) {
- var documentElement = (elm ? elm.ownerDocument || elm : 0).documentElement;
- return documentElement ? documentElement.nodeName !== 'HTML' : false;
- },
-
- /**
- * Encodes a XML node to string
- * based on http://www.mercurytide.co.uk/news/article/issues-when-working-ajax/
- */
- encode: function(xmlNode) {
- if (!this.isXML(xmlNode)) {
- return false;
- }
- try { // Mozilla, Webkit, Opera
- return new XMLSerializer().serializeToString(xmlNode);
- } catch (E1) {
- try { // IE
- return xmlNode.xml;
- } catch (E2) {}
- }
- return false;
- },
-
- /**
- * Decodes a XML node from string
- * loosely based on http://outwestmedia.com/jquery-plugins/xmldom/
- */
- decode: function(xmlString) {
- var dom_parser = ('DOMParser' in window && (new DOMParser()).parseFromString) ||
- (window.ActiveXObject && function(_xmlString) {
- var xml_doc = new ActiveXObject('Microsoft.XMLDOM');
- xml_doc.async = 'false';
- xml_doc.loadXML(_xmlString);
- return xml_doc;
- }),
- resultXML;
- if (!dom_parser) {
- return false;
- }
- resultXML = dom_parser.call('DOMParser' in window && (new DOMParser()) || window, xmlString, 'text/xml');
- return this.isXML(resultXML) ? resultXML : false;
- }
- };
-
-
- ////////////////////////// PRIVATE METHODS ////////////////////////
-
- /**
- * Initialization function. Detects if the browser supports DOM Storage
- * or userData behavior and behaves accordingly.
- */
- function _init() {
- /* Check if browser supports localStorage */
- var localStorageReallyWorks = false;
- if ('localStorage' in window) {
- try {
- window.localStorage.setItem('_tmptest', 'tmpval');
- localStorageReallyWorks = true;
- window.localStorage.removeItem('_tmptest');
- } catch (BogusQuotaExceededErrorOnIos5) {
- // Thanks be to iOS5 Private Browsing mode which throws
- // QUOTA_EXCEEDED_ERRROR DOM Exception 22.
- }
- }
-
- if (localStorageReallyWorks) {
- try {
- if (window.localStorage) {
- _storage_service = window.localStorage;
- _backend = 'localStorage';
- _observer_update = _storage_service.jStorage_update;
- }
- } catch (E3) { /* Firefox fails when touching localStorage and cookies are disabled */ }
- }
- /* Check if browser supports globalStorage */
- else if ('globalStorage' in window) {
- try {
- if (window.globalStorage) {
- if (window.location.hostname == 'localhost') {
- _storage_service = window.globalStorage['localhost.localdomain'];
- } else {
- _storage_service = window.globalStorage[window.location.hostname];
- }
- _backend = 'globalStorage';
- _observer_update = _storage_service.jStorage_update;
- }
- } catch (E4) { /* Firefox fails when touching localStorage and cookies are disabled */ }
- }
- /* Check if browser supports userData behavior */
- else {
- _storage_elm = document.createElement('link');
- if (_storage_elm.addBehavior) {
-
- /* Use a DOM element to act as userData storage */
- _storage_elm.style.behavior = 'url(#default#userData)';
-
- /* userData element needs to be inserted into the DOM! */
- document.getElementsByTagName('head')[0].appendChild(_storage_elm);
-
- try {
- _storage_elm.load('jStorage');
- } catch (E) {
- // try to reset cache
- _storage_elm.setAttribute('jStorage', '{}');
- _storage_elm.save('jStorage');
- _storage_elm.load('jStorage');
- }
-
- var data = '{}';
- try {
- data = _storage_elm.getAttribute('jStorage');
- } catch (E5) {}
-
- try {
- _observer_update = _storage_elm.getAttribute('jStorage_update');
- } catch (E6) {}
-
- _storage_service.jStorage = data;
- _backend = 'userDataBehavior';
- } else {
- _storage_elm = null;
- return;
- }
- }
-
- // Load data from storage
- _load_storage();
-
- // remove dead keys
- _handleTTL();
-
- // start listening for changes
- _setupObserver();
-
- // initialize publish-subscribe service
- _handlePubSub();
-
- // handle cached navigation
- if ('addEventListener' in window) {
- window.addEventListener('pageshow', function(event) {
- if (event.persisted) {
- _storageObserver();
- }
- }, false);
- }
- }
-
- /**
- * Reload data from storage when needed
- */
- function _reloadData() {
- var data = '{}';
-
- if (_backend == 'userDataBehavior') {
- _storage_elm.load('jStorage');
-
- try {
- data = _storage_elm.getAttribute('jStorage');
- } catch (E5) {}
-
- try {
- _observer_update = _storage_elm.getAttribute('jStorage_update');
- } catch (E6) {}
-
- _storage_service.jStorage = data;
- }
-
- _load_storage();
-
- // remove dead keys
- _handleTTL();
-
- _handlePubSub();
- }
-
- /**
- * Sets up a storage change observer
- */
- function _setupObserver() {
- if (_backend == 'localStorage' || _backend == 'globalStorage') {
- if ('addEventListener' in window) {
- window.addEventListener('storage', _storageObserver, false);
- } else {
- document.attachEvent('onstorage', _storageObserver);
- }
- } else if (_backend == 'userDataBehavior') {
- setInterval(_storageObserver, 1000);
- }
- }
-
- /**
- * Fired on any kind of data change, needs to check if anything has
- * really been changed
- */
- function _storageObserver() {
- var updateTime;
- // cumulate change notifications with timeout
- clearTimeout(_observer_timeout);
- _observer_timeout = setTimeout(function() {
-
- if (_backend == 'localStorage' || _backend == 'globalStorage') {
- updateTime = _storage_service.jStorage_update;
- } else if (_backend == 'userDataBehavior') {
- _storage_elm.load('jStorage');
- try {
- updateTime = _storage_elm.getAttribute('jStorage_update');
- } catch (E5) {}
- }
-
- if (updateTime && updateTime != _observer_update) {
- _observer_update = updateTime;
- _checkUpdatedKeys();
- }
-
- }, 25);
- }
-
- /**
- * Reloads the data and checks if any keys are changed
- */
- function _checkUpdatedKeys() {
- var oldCrc32List = JSON.parse(JSON.stringify(_storage.__jstorage_meta.CRC32)),
- newCrc32List;
-
- _reloadData();
- newCrc32List = JSON.parse(JSON.stringify(_storage.__jstorage_meta.CRC32));
-
- var key,
- updated = [],
- removed = [];
-
- for (key in oldCrc32List) {
- if (oldCrc32List.hasOwnProperty(key)) {
- if (!newCrc32List[key]) {
- removed.push(key);
- continue;
- }
- if (oldCrc32List[key] != newCrc32List[key] && String(oldCrc32List[key]).substr(0, 2) == '2.') {
- updated.push(key);
- }
- }
- }
-
- for (key in newCrc32List) {
- if (newCrc32List.hasOwnProperty(key)) {
- if (!oldCrc32List[key]) {
- updated.push(key);
- }
- }
- }
-
- _fireObservers(updated, 'updated');
- _fireObservers(removed, 'deleted');
- }
-
- /**
- * Fires observers for updated keys
- *
- * @param {Array|String} keys Array of key names or a key
- * @param {String} action What happened with the value (updated, deleted, flushed)
- */
- function _fireObservers(keys, action) {
- keys = [].concat(keys || []);
-
- var i, j, len, jlen;
-
- if (action == 'flushed') {
- keys = [];
- for (var key in _observers) {
- if (_observers.hasOwnProperty(key)) {
- keys.push(key);
- }
- }
- action = 'deleted';
- }
- for (i = 0, len = keys.length; i < len; i++) {
- if (_observers[keys[i]]) {
- for (j = 0, jlen = _observers[keys[i]].length; j < jlen; j++) {
- _observers[keys[i]][j](keys[i], action);
- }
- }
- if (_observers['*']) {
- for (j = 0, jlen = _observers['*'].length; j < jlen; j++) {
- _observers['*'][j](keys[i], action);
- }
- }
- }
- }
-
- /**
- * Publishes key change to listeners
- */
- function _publishChange() {
- var updateTime = (+new Date()).toString();
-
- if (_backend == 'localStorage' || _backend == 'globalStorage') {
- try {
- _storage_service.jStorage_update = updateTime;
- } catch (E8) {
- // safari private mode has been enabled after the jStorage initialization
- _backend = false;
- }
- } else if (_backend == 'userDataBehavior') {
- _storage_elm.setAttribute('jStorage_update', updateTime);
- _storage_elm.save('jStorage');
- }
-
- _storageObserver();
- }
-
- /**
- * Loads the data from the storage based on the supported mechanism
- */
- function _load_storage() {
- /* if jStorage string is retrieved, then decode it */
- if (_storage_service.jStorage) {
- try {
- _storage = JSON.parse(String(_storage_service.jStorage));
- } catch (E6) {
- _storage_service.jStorage = '{}';
- }
- } else {
- _storage_service.jStorage = '{}';
- }
- _storage_size = _storage_service.jStorage ? String(_storage_service.jStorage).length : 0;
-
- if (!_storage.__jstorage_meta) {
- _storage.__jstorage_meta = {};
- }
- if (!_storage.__jstorage_meta.CRC32) {
- _storage.__jstorage_meta.CRC32 = {};
- }
- }
-
- /**
- * This functions provides the 'save' mechanism to store the jStorage object
- */
- function _save() {
- _dropOldEvents(); // remove expired events
- try {
- _storage_service.jStorage = JSON.stringify(_storage);
- // If userData is used as the storage engine, additional
- if (_storage_elm) {
- _storage_elm.setAttribute('jStorage', _storage_service.jStorage);
- _storage_elm.save('jStorage');
- }
- _storage_size = _storage_service.jStorage ? String(_storage_service.jStorage).length : 0;
- } catch (E7) { /* probably cache is full, nothing is saved this way*/ }
- }
-
- /**
- * Function checks if a key is set and is string or numberic
- *
- * @param {String} key Key name
- */
- function _checkKey(key) {
- if (typeof key != 'string' && typeof key != 'number') {
- throw new TypeError('Key name must be string or numeric');
- }
- if (key == '__jstorage_meta') {
- throw new TypeError('Reserved key name');
- }
- return true;
- }
-
- /**
- * Removes expired keys
- */
- function _handleTTL() {
- var curtime, i, TTL, CRC32, nextExpire = Infinity,
- changed = false,
- deleted = [];
-
- clearTimeout(_ttl_timeout);
-
- if (!_storage.__jstorage_meta || typeof _storage.__jstorage_meta.TTL != 'object') {
- // nothing to do here
- return;
- }
-
- curtime = +new Date();
- TTL = _storage.__jstorage_meta.TTL;
-
- CRC32 = _storage.__jstorage_meta.CRC32;
- for (i in TTL) {
- if (TTL.hasOwnProperty(i)) {
- if (TTL[i] <= curtime) {
- delete TTL[i];
- delete CRC32[i];
- delete _storage[i];
- changed = true;
- deleted.push(i);
- } else if (TTL[i] < nextExpire) {
- nextExpire = TTL[i];
- }
- }
- }
-
- // set next check
- if (nextExpire != Infinity) {
- _ttl_timeout = setTimeout(_handleTTL, Math.min(nextExpire - curtime, 0x7FFFFFFF));
- }
-
- // save changes
- if (changed) {
- _save();
- _publishChange();
- _fireObservers(deleted, 'deleted');
- }
- }
-
- /**
- * Checks if there's any events on hold to be fired to listeners
- */
- function _handlePubSub() {
- var i, len;
- if (!_storage.__jstorage_meta.PubSub) {
- return;
- }
- var pubelm,
- _pubsubCurrent = _pubsub_last,
- needFired = [];
-
- for (i = len = _storage.__jstorage_meta.PubSub.length - 1; i >= 0; i--) {
- pubelm = _storage.__jstorage_meta.PubSub[i];
- if (pubelm[0] > _pubsub_last) {
- _pubsubCurrent = pubelm[0];
- needFired.unshift(pubelm);
- }
- }
-
- for (i = needFired.length - 1; i >= 0; i--) {
- _fireSubscribers(needFired[i][1], needFired[i][2]);
- }
-
- _pubsub_last = _pubsubCurrent;
- }
-
- /**
- * Fires all subscriber listeners for a pubsub channel
- *
- * @param {String} channel Channel name
- * @param {Mixed} payload Payload data to deliver
- */
- function _fireSubscribers(channel, payload) {
- if (_pubsub_observers[channel]) {
- for (var i = 0, len = _pubsub_observers[channel].length; i < len; i++) {
- // send immutable data that can't be modified by listeners
- try {
- _pubsub_observers[channel][i](channel, JSON.parse(JSON.stringify(payload)));
- } catch (E) {}
- }
- }
- }
-
- /**
- * Remove old events from the publish stream (at least 2sec old)
- */
- function _dropOldEvents() {
- if (!_storage.__jstorage_meta.PubSub) {
- return;
- }
-
- var retire = +new Date() - 2000;
-
- for (var i = 0, len = _storage.__jstorage_meta.PubSub.length; i < len; i++) {
- if (_storage.__jstorage_meta.PubSub[i][0] <= retire) {
- // deleteCount is needed for IE6
- _storage.__jstorage_meta.PubSub.splice(i, _storage.__jstorage_meta.PubSub.length - i);
- break;
- }
- }
-
- if (!_storage.__jstorage_meta.PubSub.length) {
- delete _storage.__jstorage_meta.PubSub;
- }
-
- }
-
- /**
- * Publish payload to a channel
- *
- * @param {String} channel Channel name
- * @param {Mixed} payload Payload to send to the subscribers
- */
- function _publish(channel, payload) {
- if (!_storage.__jstorage_meta) {
- _storage.__jstorage_meta = {};
- }
- if (!_storage.__jstorage_meta.PubSub) {
- _storage.__jstorage_meta.PubSub = [];
- }
-
- _storage.__jstorage_meta.PubSub.unshift([+new Date(), channel, payload]);
-
- _save();
- _publishChange();
- }
-
-
- /**
- * JS Implementation of MurmurHash2
- *
- * SOURCE: https://github.com/garycourt/murmurhash-js (MIT licensed)
- *
- * @author <a href='mailto:gary.court@gmail.com'>Gary Court</a>
- * @see http://github.com/garycourt/murmurhash-js
- * @author <a href='mailto:aappleby@gmail.com'>Austin Appleby</a>
- * @see http://sites.google.com/site/murmurhash/
- *
- * @param {string} str ASCII only
- * @param {number} seed Positive integer only
- * @return {number} 32-bit positive integer hash
- */
-
- function murmurhash2_32_gc(str, seed) {
- var
- l = str.length,
- h = seed ^ l,
- i = 0,
- k;
-
- while (l >= 4) {
- k =
- ((str.charCodeAt(i) & 0xff)) |
- ((str.charCodeAt(++i) & 0xff) << 8) |
- ((str.charCodeAt(++i) & 0xff) << 16) |
- ((str.charCodeAt(++i) & 0xff) << 24);
-
- k = (((k & 0xffff) * 0x5bd1e995) + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16));
- k ^= k >>> 24;
- k = (((k & 0xffff) * 0x5bd1e995) + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16));
-
- h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)) ^ k;
-
- l -= 4;
- ++i;
- }
-
- switch (l) {
- case 3:
- h ^= (str.charCodeAt(i + 2) & 0xff) << 16;
- /* falls through */
- case 2:
- h ^= (str.charCodeAt(i + 1) & 0xff) << 8;
- /* falls through */
- case 1:
- h ^= (str.charCodeAt(i) & 0xff);
- h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16));
- }
-
- h ^= h >>> 13;
- h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16));
- h ^= h >>> 15;
-
- return h >>> 0;
- }
-
- ////////////////////////// PUBLIC INTERFACE /////////////////////////
-
- $.jStorage = {
- /* Version number */
- version: JSTORAGE_VERSION,
-
- /**
- * Sets a key's value.
- *
- * @param {String} key Key to set. If this value is not set or not
- * a string an exception is raised.
- * @param {Mixed} value Value to set. This can be any value that is JSON
- * compatible (Numbers, Strings, Objects etc.).
- * @param {Object} [options] - possible options to use
- * @param {Number} [options.TTL] - optional TTL value, in milliseconds
- * @return {Mixed} the used value
- */
- set: function(key, value, options) {
- _checkKey(key);
-
- options = options || {};
-
- // undefined values are deleted automatically
- if (typeof value == 'undefined') {
- this.deleteKey(key);
- return value;
- }
-
- if (_XMLService.isXML(value)) {
- value = {
- _is_xml: true,
- xml: _XMLService.encode(value)
- };
- } else if (typeof value == 'function') {
- return undefined; // functions can't be saved!
- } else if (value && typeof value == 'object') {
- // clone the object before saving to _storage tree
- value = JSON.parse(JSON.stringify(value));
- }
-
- _storage[key] = value;
-
- _storage.__jstorage_meta.CRC32[key] = '2.' + murmurhash2_32_gc(JSON.stringify(value), 0x9747b28c);
-
- this.setTTL(key, options.TTL || 0); // also handles saving and _publishChange
-
- _fireObservers(key, 'updated');
- return value;
- },
-
- /**
- * Looks up a key in cache
- *
- * @param {String} key - Key to look up.
- * @param {mixed} def - Default value to return, if key didn't exist.
- * @return {Mixed} the key value, default value or null
- */
- get: function(key, def) {
- _checkKey(key);
- if (key in _storage) {
- if (_storage[key] && typeof _storage[key] == 'object' && _storage[key]._is_xml) {
- return _XMLService.decode(_storage[key].xml);
- } else {
- return _storage[key];
- }
- }
- return typeof(def) == 'undefined' ? null : def;
- },
-
- /**
- * Deletes a key from cache.
- *
- * @param {String} key - Key to delete.
- * @return {Boolean} true if key existed or false if it didn't
- */
- deleteKey: function(key) {
- _checkKey(key);
- if (key in _storage) {
- delete _storage[key];
- // remove from TTL list
- if (typeof _storage.__jstorage_meta.TTL == 'object' &&
- key in _storage.__jstorage_meta.TTL) {
- delete _storage.__jstorage_meta.TTL[key];
- }
-
- delete _storage.__jstorage_meta.CRC32[key];
-
- _save();
- _publishChange();
- _fireObservers(key, 'deleted');
- return true;
- }
- return false;
- },
-
- /**
- * Sets a TTL for a key, or remove it if ttl value is 0 or below
- *
- * @param {String} key - key to set the TTL for
- * @param {Number} ttl - TTL timeout in milliseconds
- * @return {Boolean} true if key existed or false if it didn't
- */
- setTTL: function(key, ttl) {
- var curtime = +new Date();
- _checkKey(key);
- ttl = Number(ttl) || 0;
- if (key in _storage) {
-
- if (!_storage.__jstorage_meta.TTL) {
- _storage.__jstorage_meta.TTL = {};
- }
-
- // Set TTL value for the key
- if (ttl > 0) {
- _storage.__jstorage_meta.TTL[key] = curtime + ttl;
- } else {
- delete _storage.__jstorage_meta.TTL[key];
- }
-
- _save();
-
- _handleTTL();
-
- _publishChange();
- return true;
- }
- return false;
- },
-
- /**
- * Gets remaining TTL (in milliseconds) for a key or 0 when no TTL has been set
- *
- * @param {String} key Key to check
- * @return {Number} Remaining TTL in milliseconds
- */
- getTTL: function(key) {
- var curtime = +new Date(),
- ttl;
- _checkKey(key);
- if (key in _storage && _storage.__jstorage_meta.TTL && _storage.__jstorage_meta.TTL[key]) {
- ttl = _storage.__jstorage_meta.TTL[key] - curtime;
- return ttl || 0;
- }
- return 0;
- },
-
- /**
- * Deletes everything in cache.
- *
- * @return {Boolean} Always true
- */
- flush: function() {
- _storage = {
- __jstorage_meta: {
- CRC32: {}
- }
- };
- _save();
- _publishChange();
- _fireObservers(null, 'flushed');
- return true;
- },
-
- /**
- * Returns a read-only copy of _storage
- *
- * @return {Object} Read-only copy of _storage
- */
- storageObj: function() {
- function F() {}
- F.prototype = _storage;
- return new F();
- },
-
- /**
- * Returns an index of all used keys as an array
- * ['key1', 'key2',..'keyN']
- *
- * @return {Array} Used keys
- */
- index: function() {
- var index = [],
- i;
- for (i in _storage) {
- if (_storage.hasOwnProperty(i) && i != '__jstorage_meta') {
- index.push(i);
- }
- }
- return index;
- },
-
- /**
- * How much space in bytes does the storage take?
- *
- * @return {Number} Storage size in chars (not the same as in bytes,
- * since some chars may take several bytes)
- */
- storageSize: function() {
- return _storage_size;
- },
-
- /**
- * Which backend is currently in use?
- *
- * @return {String} Backend name
- */
- currentBackend: function() {
- return _backend;
- },
-
- /**
- * Test if storage is available
- *
- * @return {Boolean} True if storage can be used
- */
- storageAvailable: function() {
- return !!_backend;
- },
-
- /**
- * Register change listeners
- *
- * @param {String} key Key name
- * @param {Function} callback Function to run when the key changes
- */
- listenKeyChange: function(key, callback) {
- _checkKey(key);
- if (!_observers[key]) {
- _observers[key] = [];
- }
- _observers[key].push(callback);
- },
-
- /**
- * Remove change listeners
- *
- * @param {String} key Key name to unregister listeners against
- * @param {Function} [callback] If set, unregister the callback, if not - unregister all
- */
- stopListening: function(key, callback) {
- _checkKey(key);
-
- if (!_observers[key]) {
- return;
- }
-
- if (!callback) {
- delete _observers[key];
- return;
- }
-
- for (var i = _observers[key].length - 1; i >= 0; i--) {
- if (_observers[key][i] == callback) {
- _observers[key].splice(i, 1);
- }
- }
- },
-
- /**
- * Subscribe to a Publish/Subscribe event stream
- *
- * @param {String} channel Channel name
- * @param {Function} callback Function to run when the something is published to the channel
- */
- subscribe: function(channel, callback) {
- channel = (channel || '').toString();
- if (!channel) {
- throw new TypeError('Channel not defined');
- }
- if (!_pubsub_observers[channel]) {
- _pubsub_observers[channel] = [];
- }
- _pubsub_observers[channel].push(callback);
- },
-
- /**
- * Publish data to an event stream
- *
- * @param {String} channel Channel name
- * @param {Mixed} payload Payload to deliver
- */
- publish: function(channel, payload) {
- channel = (channel || '').toString();
- if (!channel) {
- throw new TypeError('Channel not defined');
- }
-
- _publish(channel, payload);
- },
-
- /**
- * Reloads the data from browser storage
- */
- reInit: function() {
- _reloadData();
- },
-
- /**
- * Removes reference from global objects and saves it as jStorage
- *
- * @param {Boolean} option if needed to save object as simple 'jStorage' in windows context
- */
- noConflict: function(saveInGlobal) {
- delete window.$.jStorage;
-
- if (saveInGlobal) {
- window.jStorage = this;
- }
-
- return this;
- }
- };
-
- // Initialize jStorage
- _init();
-
-})();
--- /dev/null
+/*
+ * ----------------------------- JSTORAGE -------------------------------------
+ * Simple local storage wrapper to save data on the browser side, supporting
+ * all major browsers - IE6+, Firefox2+, Safari4+, Chrome4+ and Opera 10.5+
+ *
+ * Author: Andris Reinman, andris.reinman@gmail.com
+ * Project homepage: www.jstorage.info
+ *
+ * Licensed under Unlicense:
+ *
+ * This is free and unencumbered software released into the public domain.
+ *
+ * Anyone is free to copy, modify, publish, use, compile, sell, or
+ * distribute this software, either in source code form or as a compiled
+ * binary, for any purpose, commercial or non-commercial, and by any
+ * means.
+ *
+ * In jurisdictions that recognize copyright laws, the author or authors
+ * of this software dedicate any and all copyright interest in the
+ * software to the public domain. We make this dedication for the benefit
+ * of the public at large and to the detriment of our heirs and
+ * successors. We intend this dedication to be an overt act of
+ * relinquishment in perpetuity of all present and future rights to this
+ * software under copyright law.
+ *
+ * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+ * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * For more information, please refer to <http://unlicense.org/>
+ */
+
+/* global ActiveXObject: false */
+/* jshint browser: true */
+
+(function() {
+ 'use strict';
+
+ var
+ /* jStorage version */
+ JSTORAGE_VERSION = '0.4.12',
+
+ /* detect a dollar object or create one if not found */
+ $ = window.jQuery || window.$ || (window.$ = {}),
+
+ /* check for a JSON handling support */
+ JSON = {
+ parse: window.JSON && (window.JSON.parse || window.JSON.decode) ||
+ String.prototype.evalJSON && function(str) {
+ return String(str).evalJSON();
+ } ||
+ $.parseJSON ||
+ $.evalJSON,
+ stringify: Object.toJSON ||
+ window.JSON && (window.JSON.stringify || window.JSON.encode) ||
+ $.toJSON
+ };
+
+ // Break if no JSON support was found
+ if (typeof JSON.parse !== 'function' || typeof JSON.stringify !== 'function') {
+ throw new Error('No JSON support found, include //cdnjs.cloudflare.com/ajax/libs/json2/20110223/json2.js to page');
+ }
+
+ var
+ /* This is the object, that holds the cached values */
+ _storage = {
+ __jstorage_meta: {
+ CRC32: {}
+ }
+ },
+
+ /* Actual browser storage (localStorage or globalStorage['domain']) */
+ _storage_service = {
+ jStorage: '{}'
+ },
+
+ /* DOM element for older IE versions, holds userData behavior */
+ _storage_elm = null,
+
+ /* How much space does the storage take */
+ _storage_size = 0,
+
+ /* which backend is currently used */
+ _backend = false,
+
+ /* onchange observers */
+ _observers = {},
+
+ /* timeout to wait after onchange event */
+ _observer_timeout = false,
+
+ /* last update time */
+ _observer_update = 0,
+
+ /* pubsub observers */
+ _pubsub_observers = {},
+
+ /* skip published items older than current timestamp */
+ _pubsub_last = +new Date(),
+
+ /* Next check for TTL */
+ _ttl_timeout,
+
+ /**
+ * XML encoding and decoding as XML nodes can't be JSON'ized
+ * XML nodes are encoded and decoded if the node is the value to be saved
+ * but not if it's as a property of another object
+ * Eg. -
+ * $.jStorage.set('key', xmlNode); // IS OK
+ * $.jStorage.set('key', {xml: xmlNode}); // NOT OK
+ */
+ _XMLService = {
+
+ /**
+ * Validates a XML node to be XML
+ * based on jQuery.isXML function
+ */
+ isXML: function(elm) {
+ var documentElement = (elm ? elm.ownerDocument || elm : 0).documentElement;
+ return documentElement ? documentElement.nodeName !== 'HTML' : false;
+ },
+
+ /**
+ * Encodes a XML node to string
+ * based on http://www.mercurytide.co.uk/news/article/issues-when-working-ajax/
+ */
+ encode: function(xmlNode) {
+ if (!this.isXML(xmlNode)) {
+ return false;
+ }
+ try { // Mozilla, Webkit, Opera
+ return new XMLSerializer().serializeToString(xmlNode);
+ } catch (E1) {
+ try { // IE
+ return xmlNode.xml;
+ } catch (E2) {}
+ }
+ return false;
+ },
+
+ /**
+ * Decodes a XML node from string
+ * loosely based on http://outwestmedia.com/jquery-plugins/xmldom/
+ */
+ decode: function(xmlString) {
+ var dom_parser = ('DOMParser' in window && (new DOMParser()).parseFromString) ||
+ (window.ActiveXObject && function(_xmlString) {
+ var xml_doc = new ActiveXObject('Microsoft.XMLDOM');
+ xml_doc.async = 'false';
+ xml_doc.loadXML(_xmlString);
+ return xml_doc;
+ }),
+ resultXML;
+ if (!dom_parser) {
+ return false;
+ }
+ resultXML = dom_parser.call('DOMParser' in window && (new DOMParser()) || window, xmlString, 'text/xml');
+ return this.isXML(resultXML) ? resultXML : false;
+ }
+ };
+
+
+ ////////////////////////// PRIVATE METHODS ////////////////////////
+
+ /**
+ * Initialization function. Detects if the browser supports DOM Storage
+ * or userData behavior and behaves accordingly.
+ */
+ function _init() {
+ /* Check if browser supports localStorage */
+ var localStorageReallyWorks = false;
+ if ('localStorage' in window) {
+ try {
+ window.localStorage.setItem('_tmptest', 'tmpval');
+ localStorageReallyWorks = true;
+ window.localStorage.removeItem('_tmptest');
+ } catch (BogusQuotaExceededErrorOnIos5) {
+ // Thanks be to iOS5 Private Browsing mode which throws
+ // QUOTA_EXCEEDED_ERRROR DOM Exception 22.
+ }
+ }
+
+ if (localStorageReallyWorks) {
+ try {
+ if (window.localStorage) {
+ _storage_service = window.localStorage;
+ _backend = 'localStorage';
+ _observer_update = _storage_service.jStorage_update;
+ }
+ } catch (E3) { /* Firefox fails when touching localStorage and cookies are disabled */ }
+ }
+ /* Check if browser supports globalStorage */
+ else if ('globalStorage' in window) {
+ try {
+ if (window.globalStorage) {
+ if (window.location.hostname == 'localhost') {
+ _storage_service = window.globalStorage['localhost.localdomain'];
+ } else {
+ _storage_service = window.globalStorage[window.location.hostname];
+ }
+ _backend = 'globalStorage';
+ _observer_update = _storage_service.jStorage_update;
+ }
+ } catch (E4) { /* Firefox fails when touching localStorage and cookies are disabled */ }
+ }
+ /* Check if browser supports userData behavior */
+ else {
+ _storage_elm = document.createElement('link');
+ if (_storage_elm.addBehavior) {
+
+ /* Use a DOM element to act as userData storage */
+ _storage_elm.style.behavior = 'url(#default#userData)';
+
+ /* userData element needs to be inserted into the DOM! */
+ document.getElementsByTagName('head')[0].appendChild(_storage_elm);
+
+ try {
+ _storage_elm.load('jStorage');
+ } catch (E) {
+ // try to reset cache
+ _storage_elm.setAttribute('jStorage', '{}');
+ _storage_elm.save('jStorage');
+ _storage_elm.load('jStorage');
+ }
+
+ var data = '{}';
+ try {
+ data = _storage_elm.getAttribute('jStorage');
+ } catch (E5) {}
+
+ try {
+ _observer_update = _storage_elm.getAttribute('jStorage_update');
+ } catch (E6) {}
+
+ _storage_service.jStorage = data;
+ _backend = 'userDataBehavior';
+ } else {
+ _storage_elm = null;
+ return;
+ }
+ }
+
+ // Load data from storage
+ _load_storage();
+
+ // remove dead keys
+ _handleTTL();
+
+ // start listening for changes
+ _setupObserver();
+
+ // initialize publish-subscribe service
+ _handlePubSub();
+
+ // handle cached navigation
+ if ('addEventListener' in window) {
+ window.addEventListener('pageshow', function(event) {
+ if (event.persisted) {
+ _storageObserver();
+ }
+ }, false);
+ }
+ }
+
+ /**
+ * Reload data from storage when needed
+ */
+ function _reloadData() {
+ var data = '{}';
+
+ if (_backend == 'userDataBehavior') {
+ _storage_elm.load('jStorage');
+
+ try {
+ data = _storage_elm.getAttribute('jStorage');
+ } catch (E5) {}
+
+ try {
+ _observer_update = _storage_elm.getAttribute('jStorage_update');
+ } catch (E6) {}
+
+ _storage_service.jStorage = data;
+ }
+
+ _load_storage();
+
+ // remove dead keys
+ _handleTTL();
+
+ _handlePubSub();
+ }
+
+ /**
+ * Sets up a storage change observer
+ */
+ function _setupObserver() {
+ if (_backend == 'localStorage' || _backend == 'globalStorage') {
+ if ('addEventListener' in window) {
+ window.addEventListener('storage', _storageObserver, false);
+ } else {
+ document.attachEvent('onstorage', _storageObserver);
+ }
+ } else if (_backend == 'userDataBehavior') {
+ setInterval(_storageObserver, 1000);
+ }
+ }
+
+ /**
+ * Fired on any kind of data change, needs to check if anything has
+ * really been changed
+ */
+ function _storageObserver() {
+ var updateTime;
+ // cumulate change notifications with timeout
+ clearTimeout(_observer_timeout);
+ _observer_timeout = setTimeout(function() {
+
+ if (_backend == 'localStorage' || _backend == 'globalStorage') {
+ updateTime = _storage_service.jStorage_update;
+ } else if (_backend == 'userDataBehavior') {
+ _storage_elm.load('jStorage');
+ try {
+ updateTime = _storage_elm.getAttribute('jStorage_update');
+ } catch (E5) {}
+ }
+
+ if (updateTime && updateTime != _observer_update) {
+ _observer_update = updateTime;
+ _checkUpdatedKeys();
+ }
+
+ }, 25);
+ }
+
+ /**
+ * Reloads the data and checks if any keys are changed
+ */
+ function _checkUpdatedKeys() {
+ var oldCrc32List = JSON.parse(JSON.stringify(_storage.__jstorage_meta.CRC32)),
+ newCrc32List;
+
+ _reloadData();
+ newCrc32List = JSON.parse(JSON.stringify(_storage.__jstorage_meta.CRC32));
+
+ var key,
+ updated = [],
+ removed = [];
+
+ for (key in oldCrc32List) {
+ if (oldCrc32List.hasOwnProperty(key)) {
+ if (!newCrc32List[key]) {
+ removed.push(key);
+ continue;
+ }
+ if (oldCrc32List[key] != newCrc32List[key] && String(oldCrc32List[key]).substr(0, 2) == '2.') {
+ updated.push(key);
+ }
+ }
+ }
+
+ for (key in newCrc32List) {
+ if (newCrc32List.hasOwnProperty(key)) {
+ if (!oldCrc32List[key]) {
+ updated.push(key);
+ }
+ }
+ }
+
+ _fireObservers(updated, 'updated');
+ _fireObservers(removed, 'deleted');
+ }
+
+ /**
+ * Fires observers for updated keys
+ *
+ * @param {Array|String} keys Array of key names or a key
+ * @param {String} action What happened with the value (updated, deleted, flushed)
+ */
+ function _fireObservers(keys, action) {
+ keys = [].concat(keys || []);
+
+ var i, j, len, jlen;
+
+ if (action == 'flushed') {
+ keys = [];
+ for (var key in _observers) {
+ if (_observers.hasOwnProperty(key)) {
+ keys.push(key);
+ }
+ }
+ action = 'deleted';
+ }
+ for (i = 0, len = keys.length; i < len; i++) {
+ if (_observers[keys[i]]) {
+ for (j = 0, jlen = _observers[keys[i]].length; j < jlen; j++) {
+ _observers[keys[i]][j](keys[i], action);
+ }
+ }
+ if (_observers['*']) {
+ for (j = 0, jlen = _observers['*'].length; j < jlen; j++) {
+ _observers['*'][j](keys[i], action);
+ }
+ }
+ }
+ }
+
+ /**
+ * Publishes key change to listeners
+ */
+ function _publishChange() {
+ var updateTime = (+new Date()).toString();
+
+ if (_backend == 'localStorage' || _backend == 'globalStorage') {
+ try {
+ _storage_service.jStorage_update = updateTime;
+ } catch (E8) {
+ // safari private mode has been enabled after the jStorage initialization
+ _backend = false;
+ }
+ } else if (_backend == 'userDataBehavior') {
+ _storage_elm.setAttribute('jStorage_update', updateTime);
+ _storage_elm.save('jStorage');
+ }
+
+ _storageObserver();
+ }
+
+ /**
+ * Loads the data from the storage based on the supported mechanism
+ */
+ function _load_storage() {
+ /* if jStorage string is retrieved, then decode it */
+ if (_storage_service.jStorage) {
+ try {
+ _storage = JSON.parse(String(_storage_service.jStorage));
+ } catch (E6) {
+ _storage_service.jStorage = '{}';
+ }
+ } else {
+ _storage_service.jStorage = '{}';
+ }
+ _storage_size = _storage_service.jStorage ? String(_storage_service.jStorage).length : 0;
+
+ if (!_storage.__jstorage_meta) {
+ _storage.__jstorage_meta = {};
+ }
+ if (!_storage.__jstorage_meta.CRC32) {
+ _storage.__jstorage_meta.CRC32 = {};
+ }
+ }
+
+ /**
+ * This functions provides the 'save' mechanism to store the jStorage object
+ */
+ function _save() {
+ _dropOldEvents(); // remove expired events
+ try {
+ _storage_service.jStorage = JSON.stringify(_storage);
+ // If userData is used as the storage engine, additional
+ if (_storage_elm) {
+ _storage_elm.setAttribute('jStorage', _storage_service.jStorage);
+ _storage_elm.save('jStorage');
+ }
+ _storage_size = _storage_service.jStorage ? String(_storage_service.jStorage).length : 0;
+ } catch (E7) { /* probably cache is full, nothing is saved this way*/ }
+ }
+
+ /**
+ * Function checks if a key is set and is string or numberic
+ *
+ * @param {String} key Key name
+ */
+ function _checkKey(key) {
+ if (typeof key != 'string' && typeof key != 'number') {
+ throw new TypeError('Key name must be string or numeric');
+ }
+ if (key == '__jstorage_meta') {
+ throw new TypeError('Reserved key name');
+ }
+ return true;
+ }
+
+ /**
+ * Removes expired keys
+ */
+ function _handleTTL() {
+ var curtime, i, TTL, CRC32, nextExpire = Infinity,
+ changed = false,
+ deleted = [];
+
+ clearTimeout(_ttl_timeout);
+
+ if (!_storage.__jstorage_meta || typeof _storage.__jstorage_meta.TTL != 'object') {
+ // nothing to do here
+ return;
+ }
+
+ curtime = +new Date();
+ TTL = _storage.__jstorage_meta.TTL;
+
+ CRC32 = _storage.__jstorage_meta.CRC32;
+ for (i in TTL) {
+ if (TTL.hasOwnProperty(i)) {
+ if (TTL[i] <= curtime) {
+ delete TTL[i];
+ delete CRC32[i];
+ delete _storage[i];
+ changed = true;
+ deleted.push(i);
+ } else if (TTL[i] < nextExpire) {
+ nextExpire = TTL[i];
+ }
+ }
+ }
+
+ // set next check
+ if (nextExpire != Infinity) {
+ _ttl_timeout = setTimeout(_handleTTL, Math.min(nextExpire - curtime, 0x7FFFFFFF));
+ }
+
+ // save changes
+ if (changed) {
+ _save();
+ _publishChange();
+ _fireObservers(deleted, 'deleted');
+ }
+ }
+
+ /**
+ * Checks if there's any events on hold to be fired to listeners
+ */
+ function _handlePubSub() {
+ var i, len;
+ if (!_storage.__jstorage_meta.PubSub) {
+ return;
+ }
+ var pubelm,
+ _pubsubCurrent = _pubsub_last,
+ needFired = [];
+
+ for (i = len = _storage.__jstorage_meta.PubSub.length - 1; i >= 0; i--) {
+ pubelm = _storage.__jstorage_meta.PubSub[i];
+ if (pubelm[0] > _pubsub_last) {
+ _pubsubCurrent = pubelm[0];
+ needFired.unshift(pubelm);
+ }
+ }
+
+ for (i = needFired.length - 1; i >= 0; i--) {
+ _fireSubscribers(needFired[i][1], needFired[i][2]);
+ }
+
+ _pubsub_last = _pubsubCurrent;
+ }
+
+ /**
+ * Fires all subscriber listeners for a pubsub channel
+ *
+ * @param {String} channel Channel name
+ * @param {Mixed} payload Payload data to deliver
+ */
+ function _fireSubscribers(channel, payload) {
+ if (_pubsub_observers[channel]) {
+ for (var i = 0, len = _pubsub_observers[channel].length; i < len; i++) {
+ // send immutable data that can't be modified by listeners
+ try {
+ _pubsub_observers[channel][i](channel, JSON.parse(JSON.stringify(payload)));
+ } catch (E) {}
+ }
+ }
+ }
+
+ /**
+ * Remove old events from the publish stream (at least 2sec old)
+ */
+ function _dropOldEvents() {
+ if (!_storage.__jstorage_meta.PubSub) {
+ return;
+ }
+
+ var retire = +new Date() - 2000;
+
+ for (var i = 0, len = _storage.__jstorage_meta.PubSub.length; i < len; i++) {
+ if (_storage.__jstorage_meta.PubSub[i][0] <= retire) {
+ // deleteCount is needed for IE6
+ _storage.__jstorage_meta.PubSub.splice(i, _storage.__jstorage_meta.PubSub.length - i);
+ break;
+ }
+ }
+
+ if (!_storage.__jstorage_meta.PubSub.length) {
+ delete _storage.__jstorage_meta.PubSub;
+ }
+
+ }
+
+ /**
+ * Publish payload to a channel
+ *
+ * @param {String} channel Channel name
+ * @param {Mixed} payload Payload to send to the subscribers
+ */
+ function _publish(channel, payload) {
+ if (!_storage.__jstorage_meta) {
+ _storage.__jstorage_meta = {};
+ }
+ if (!_storage.__jstorage_meta.PubSub) {
+ _storage.__jstorage_meta.PubSub = [];
+ }
+
+ _storage.__jstorage_meta.PubSub.unshift([+new Date(), channel, payload]);
+
+ _save();
+ _publishChange();
+ }
+
+
+ /**
+ * JS Implementation of MurmurHash2
+ *
+ * SOURCE: https://github.com/garycourt/murmurhash-js (MIT licensed)
+ *
+ * @author <a href='mailto:gary.court@gmail.com'>Gary Court</a>
+ * @see http://github.com/garycourt/murmurhash-js
+ * @author <a href='mailto:aappleby@gmail.com'>Austin Appleby</a>
+ * @see http://sites.google.com/site/murmurhash/
+ *
+ * @param {string} str ASCII only
+ * @param {number} seed Positive integer only
+ * @return {number} 32-bit positive integer hash
+ */
+
+ function murmurhash2_32_gc(str, seed) {
+ var
+ l = str.length,
+ h = seed ^ l,
+ i = 0,
+ k;
+
+ while (l >= 4) {
+ k =
+ ((str.charCodeAt(i) & 0xff)) |
+ ((str.charCodeAt(++i) & 0xff) << 8) |
+ ((str.charCodeAt(++i) & 0xff) << 16) |
+ ((str.charCodeAt(++i) & 0xff) << 24);
+
+ k = (((k & 0xffff) * 0x5bd1e995) + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16));
+ k ^= k >>> 24;
+ k = (((k & 0xffff) * 0x5bd1e995) + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16));
+
+ h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)) ^ k;
+
+ l -= 4;
+ ++i;
+ }
+
+ switch (l) {
+ case 3:
+ h ^= (str.charCodeAt(i + 2) & 0xff) << 16;
+ /* falls through */
+ case 2:
+ h ^= (str.charCodeAt(i + 1) & 0xff) << 8;
+ /* falls through */
+ case 1:
+ h ^= (str.charCodeAt(i) & 0xff);
+ h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16));
+ }
+
+ h ^= h >>> 13;
+ h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16));
+ h ^= h >>> 15;
+
+ return h >>> 0;
+ }
+
+ ////////////////////////// PUBLIC INTERFACE /////////////////////////
+
+ $.jStorage = {
+ /* Version number */
+ version: JSTORAGE_VERSION,
+
+ /**
+ * Sets a key's value.
+ *
+ * @param {String} key Key to set. If this value is not set or not
+ * a string an exception is raised.
+ * @param {Mixed} value Value to set. This can be any value that is JSON
+ * compatible (Numbers, Strings, Objects etc.).
+ * @param {Object} [options] - possible options to use
+ * @param {Number} [options.TTL] - optional TTL value, in milliseconds
+ * @return {Mixed} the used value
+ */
+ set: function(key, value, options) {
+ _checkKey(key);
+
+ options = options || {};
+
+ // undefined values are deleted automatically
+ if (typeof value == 'undefined') {
+ this.deleteKey(key);
+ return value;
+ }
+
+ if (_XMLService.isXML(value)) {
+ value = {
+ _is_xml: true,
+ xml: _XMLService.encode(value)
+ };
+ } else if (typeof value == 'function') {
+ return undefined; // functions can't be saved!
+ } else if (value && typeof value == 'object') {
+ // clone the object before saving to _storage tree
+ value = JSON.parse(JSON.stringify(value));
+ }
+
+ _storage[key] = value;
+
+ _storage.__jstorage_meta.CRC32[key] = '2.' + murmurhash2_32_gc(JSON.stringify(value), 0x9747b28c);
+
+ this.setTTL(key, options.TTL || 0); // also handles saving and _publishChange
+
+ _fireObservers(key, 'updated');
+ return value;
+ },
+
+ /**
+ * Looks up a key in cache
+ *
+ * @param {String} key - Key to look up.
+ * @param {mixed} def - Default value to return, if key didn't exist.
+ * @return {Mixed} the key value, default value or null
+ */
+ get: function(key, def) {
+ _checkKey(key);
+ if (key in _storage) {
+ if (_storage[key] && typeof _storage[key] == 'object' && _storage[key]._is_xml) {
+ return _XMLService.decode(_storage[key].xml);
+ } else {
+ return _storage[key];
+ }
+ }
+ return typeof(def) == 'undefined' ? null : def;
+ },
+
+ /**
+ * Deletes a key from cache.
+ *
+ * @param {String} key - Key to delete.
+ * @return {Boolean} true if key existed or false if it didn't
+ */
+ deleteKey: function(key) {
+ _checkKey(key);
+ if (key in _storage) {
+ delete _storage[key];
+ // remove from TTL list
+ if (typeof _storage.__jstorage_meta.TTL == 'object' &&
+ key in _storage.__jstorage_meta.TTL) {
+ delete _storage.__jstorage_meta.TTL[key];
+ }
+
+ delete _storage.__jstorage_meta.CRC32[key];
+
+ _save();
+ _publishChange();
+ _fireObservers(key, 'deleted');
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Sets a TTL for a key, or remove it if ttl value is 0 or below
+ *
+ * @param {String} key - key to set the TTL for
+ * @param {Number} ttl - TTL timeout in milliseconds
+ * @return {Boolean} true if key existed or false if it didn't
+ */
+ setTTL: function(key, ttl) {
+ var curtime = +new Date();
+ _checkKey(key);
+ ttl = Number(ttl) || 0;
+ if (key in _storage) {
+
+ if (!_storage.__jstorage_meta.TTL) {
+ _storage.__jstorage_meta.TTL = {};
+ }
+
+ // Set TTL value for the key
+ if (ttl > 0) {
+ _storage.__jstorage_meta.TTL[key] = curtime + ttl;
+ } else {
+ delete _storage.__jstorage_meta.TTL[key];
+ }
+
+ _save();
+
+ _handleTTL();
+
+ _publishChange();
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Gets remaining TTL (in milliseconds) for a key or 0 when no TTL has been set
+ *
+ * @param {String} key Key to check
+ * @return {Number} Remaining TTL in milliseconds
+ */
+ getTTL: function(key) {
+ var curtime = +new Date(),
+ ttl;
+ _checkKey(key);
+ if (key in _storage && _storage.__jstorage_meta.TTL && _storage.__jstorage_meta.TTL[key]) {
+ ttl = _storage.__jstorage_meta.TTL[key] - curtime;
+ return ttl || 0;
+ }
+ return 0;
+ },
+
+ /**
+ * Deletes everything in cache.
+ *
+ * @return {Boolean} Always true
+ */
+ flush: function() {
+ _storage = {
+ __jstorage_meta: {
+ CRC32: {}
+ }
+ };
+ _save();
+ _publishChange();
+ _fireObservers(null, 'flushed');
+ return true;
+ },
+
+ /**
+ * Returns a read-only copy of _storage
+ *
+ * @return {Object} Read-only copy of _storage
+ */
+ storageObj: function() {
+ function F() {}
+ F.prototype = _storage;
+ return new F();
+ },
+
+ /**
+ * Returns an index of all used keys as an array
+ * ['key1', 'key2',..'keyN']
+ *
+ * @return {Array} Used keys
+ */
+ index: function() {
+ var index = [],
+ i;
+ for (i in _storage) {
+ if (_storage.hasOwnProperty(i) && i != '__jstorage_meta') {
+ index.push(i);
+ }
+ }
+ return index;
+ },
+
+ /**
+ * How much space in bytes does the storage take?
+ *
+ * @return {Number} Storage size in chars (not the same as in bytes,
+ * since some chars may take several bytes)
+ */
+ storageSize: function() {
+ return _storage_size;
+ },
+
+ /**
+ * Which backend is currently in use?
+ *
+ * @return {String} Backend name
+ */
+ currentBackend: function() {
+ return _backend;
+ },
+
+ /**
+ * Test if storage is available
+ *
+ * @return {Boolean} True if storage can be used
+ */
+ storageAvailable: function() {
+ return !!_backend;
+ },
+
+ /**
+ * Register change listeners
+ *
+ * @param {String} key Key name
+ * @param {Function} callback Function to run when the key changes
+ */
+ listenKeyChange: function(key, callback) {
+ _checkKey(key);
+ if (!_observers[key]) {
+ _observers[key] = [];
+ }
+ _observers[key].push(callback);
+ },
+
+ /**
+ * Remove change listeners
+ *
+ * @param {String} key Key name to unregister listeners against
+ * @param {Function} [callback] If set, unregister the callback, if not - unregister all
+ */
+ stopListening: function(key, callback) {
+ _checkKey(key);
+
+ if (!_observers[key]) {
+ return;
+ }
+
+ if (!callback) {
+ delete _observers[key];
+ return;
+ }
+
+ for (var i = _observers[key].length - 1; i >= 0; i--) {
+ if (_observers[key][i] == callback) {
+ _observers[key].splice(i, 1);
+ }
+ }
+ },
+
+ /**
+ * Subscribe to a Publish/Subscribe event stream
+ *
+ * @param {String} channel Channel name
+ * @param {Function} callback Function to run when the something is published to the channel
+ */
+ subscribe: function(channel, callback) {
+ channel = (channel || '').toString();
+ if (!channel) {
+ throw new TypeError('Channel not defined');
+ }
+ if (!_pubsub_observers[channel]) {
+ _pubsub_observers[channel] = [];
+ }
+ _pubsub_observers[channel].push(callback);
+ },
+
+ /**
+ * Publish data to an event stream
+ *
+ * @param {String} channel Channel name
+ * @param {Mixed} payload Payload to deliver
+ */
+ publish: function(channel, payload) {
+ channel = (channel || '').toString();
+ if (!channel) {
+ throw new TypeError('Channel not defined');
+ }
+
+ _publish(channel, payload);
+ },
+
+ /**
+ * Reloads the data from browser storage
+ */
+ reInit: function() {
+ _reloadData();
+ },
+
+ /**
+ * Removes reference from global objects and saves it as jStorage
+ *
+ * @param {Boolean} option if needed to save object as simple 'jStorage' in windows context
+ */
+ noConflict: function(saveInGlobal) {
+ delete window.$.jStorage;
+
+ if (saveInGlobal) {
+ window.jStorage = this;
+ }
+
+ return this;
+ }
+ };
+
+ // Initialize jStorage
+ _init();
+
+})();
\ No newline at end of file
--- /dev/null
+/*!
+ * jQuery throttle / debounce - v1.1 - 3/7/2010
+ * http://benalman.com/projects/jquery-throttle-debounce-plugin/
+ *
+ * Copyright (c) 2010 "Cowboy" Ben Alman
+ * Dual licensed under the MIT and GPL licenses.
+ * http://benalman.com/about/license/
+ */
+
+// Script: jQuery throttle / debounce: Sometimes, less is more!
+//
+// *Version: 1.1, Last updated: 3/7/2010*
+//
+// Project Home - http://benalman.com/projects/jquery-throttle-debounce-plugin/
+// GitHub - http://github.com/cowboy/jquery-throttle-debounce/
+// Source - http://github.com/cowboy/jquery-throttle-debounce/raw/master/jquery.ba-throttle-debounce.js
+// (Minified) - http://github.com/cowboy/jquery-throttle-debounce/raw/master/jquery.ba-throttle-debounce.min.js (0.7kb)
+//
+// About: License
+//
+// Copyright (c) 2010 "Cowboy" Ben Alman,
+// Dual licensed under the MIT and GPL licenses.
+// http://benalman.com/about/license/
+//
+// About: Examples
+//
+// These working examples, complete with fully commented code, illustrate a few
+// ways in which this plugin can be used.
+//
+// Throttle - http://benalman.com/code/projects/jquery-throttle-debounce/examples/throttle/
+// Debounce - http://benalman.com/code/projects/jquery-throttle-debounce/examples/debounce/
+//
+// About: Support and Testing
+//
+// Information about what version or versions of jQuery this plugin has been
+// tested with, what browsers it has been tested in, and where the unit tests
+// reside (so you can test it yourself).
+//
+// jQuery Versions - none, 1.3.2, 1.4.2
+// Browsers Tested - Internet Explorer 6-8, Firefox 2-3.6, Safari 3-4, Chrome 4-5, Opera 9.6-10.1.
+// Unit Tests - http://benalman.com/code/projects/jquery-throttle-debounce/unit/
+//
+// About: Release History
+//
+// 1.1 - (3/7/2010) Fixed a bug in <jQuery.throttle> where trailing callbacks
+// executed later than they should. Reworked a fair amount of internal
+// logic as well.
+// 1.0 - (3/6/2010) Initial release as a stand-alone project. Migrated over
+// from jquery-misc repo v0.4 to jquery-throttle repo v1.0, added the
+// no_trailing throttle parameter and debounce functionality.
+//
+// Topic: Note for non-jQuery users
+//
+// jQuery isn't actually required for this plugin, because nothing internal
+// uses any jQuery methods or properties. jQuery is just used as a namespace
+// under which these methods can exist.
+//
+// Since jQuery isn't actually required for this plugin, if jQuery doesn't exist
+// when this plugin is loaded, the method described below will be created in
+// the `Cowboy` namespace. Usage will be exactly the same, but instead of
+// $.method() or jQuery.method(), you'll need to use Cowboy.method().
+
+(function(window,undefined){
+ '$:nomunge'; // Used by YUI compressor.
+
+ // Since jQuery really isn't required for this plugin, use `jQuery` as the
+ // namespace only if it already exists, otherwise use the `Cowboy` namespace,
+ // creating it if necessary.
+ var $ = window.jQuery || window.Cowboy || ( window.Cowboy = {} ),
+
+ // Internal method reference.
+ jq_throttle;
+
+ // Method: jQuery.throttle
+ //
+ // Throttle execution of a function. Especially useful for rate limiting
+ // execution of handlers on events like resize and scroll. If you want to
+ // rate-limit execution of a function to a single time, see the
+ // <jQuery.debounce> method.
+ //
+ // In this visualization, | is a throttled-function call and X is the actual
+ // callback execution:
+ //
+ // > Throttled with `no_trailing` specified as false or unspecified:
+ // > ||||||||||||||||||||||||| (pause) |||||||||||||||||||||||||
+ // > X X X X X X X X X X X X
+ // >
+ // > Throttled with `no_trailing` specified as true:
+ // > ||||||||||||||||||||||||| (pause) |||||||||||||||||||||||||
+ // > X X X X X X X X X X
+ //
+ // Usage:
+ //
+ // > var throttled = jQuery.throttle( delay, [ no_trailing, ] callback );
+ // >
+ // > jQuery('selector').bind( 'someevent', throttled );
+ // > jQuery('selector').unbind( 'someevent', throttled );
+ //
+ // This also works in jQuery 1.4+:
+ //
+ // > jQuery('selector').bind( 'someevent', jQuery.throttle( delay, [ no_trailing, ] callback ) );
+ // > jQuery('selector').unbind( 'someevent', callback );
+ //
+ // Arguments:
+ //
+ // delay - (Number) A zero-or-greater delay in milliseconds. For event
+ // callbacks, values around 100 or 250 (or even higher) are most useful.
+ // no_trailing - (Boolean) Optional, defaults to false. If no_trailing is
+ // true, callback will only execute every `delay` milliseconds while the
+ // throttled-function is being called. If no_trailing is false or
+ // unspecified, callback will be executed one final time after the last
+ // throttled-function call. (After the throttled-function has not been
+ // called for `delay` milliseconds, the internal counter is reset)
+ // callback - (Function) A function to be executed after delay milliseconds.
+ // The `this` context and all arguments are passed through, as-is, to
+ // `callback` when the throttled-function is executed.
+ //
+ // Returns:
+ //
+ // (Function) A new, throttled, function.
+
+ $.throttle = jq_throttle = function( delay, no_trailing, callback, debounce_mode ) {
+ // After wrapper has stopped being called, this timeout ensures that
+ // `callback` is executed at the proper times in `throttle` and `end`
+ // debounce modes.
+ var timeout_id,
+
+ // Keep track of the last time `callback` was executed.
+ last_exec = 0;
+
+ // `no_trailing` defaults to falsy.
+ if ( typeof no_trailing !== 'boolean' ) {
+ debounce_mode = callback;
+ callback = no_trailing;
+ no_trailing = undefined;
+ }
+
+ // The `wrapper` function encapsulates all of the throttling / debouncing
+ // functionality and when executed will limit the rate at which `callback`
+ // is executed.
+ function wrapper() {
+ var that = this,
+ elapsed = +new Date() - last_exec,
+ args = arguments;
+
+ // Execute `callback` and update the `last_exec` timestamp.
+ function exec() {
+ last_exec = +new Date();
+ callback.apply( that, args );
+ };
+
+ // If `debounce_mode` is true (at_begin) this is used to clear the flag
+ // to allow future `callback` executions.
+ function clear() {
+ timeout_id = undefined;
+ };
+
+ if ( debounce_mode && !timeout_id ) {
+ // Since `wrapper` is being called for the first time and
+ // `debounce_mode` is true (at_begin), execute `callback`.
+ exec();
+ }
+
+ // Clear any existing timeout.
+ timeout_id && clearTimeout( timeout_id );
+
+ if ( debounce_mode === undefined && elapsed > delay ) {
+ // In throttle mode, if `delay` time has been exceeded, execute
+ // `callback`.
+ exec();
+
+ } else if ( no_trailing !== true ) {
+ // In trailing throttle mode, since `delay` time has not been
+ // exceeded, schedule `callback` to execute `delay` ms after most
+ // recent execution.
+ //
+ // If `debounce_mode` is true (at_begin), schedule `clear` to execute
+ // after `delay` ms.
+ //
+ // If `debounce_mode` is false (at end), schedule `callback` to
+ // execute after `delay` ms.
+ timeout_id = setTimeout( debounce_mode ? clear : exec, debounce_mode === undefined ? delay - elapsed : delay );
+ }
+ };
+
+ // Set the guid of `wrapper` function to the same of original callback, so
+ // it can be removed in jQuery 1.4+ .unbind or .die by using the original
+ // callback as a reference.
+ if ( $.guid ) {
+ wrapper.guid = callback.guid = callback.guid || $.guid++;
+ }
+
+ // Return the wrapper function.
+ return wrapper;
+ };
+
+ // Method: jQuery.debounce
+ //
+ // Debounce execution of a function. Debouncing, unlike throttling,
+ // guarantees that a function is only executed a single time, either at the
+ // very beginning of a series of calls, or at the very end. If you want to
+ // simply rate-limit execution of a function, see the <jQuery.throttle>
+ // method.
+ //
+ // In this visualization, | is a debounced-function call and X is the actual
+ // callback execution:
+ //
+ // > Debounced with `at_begin` specified as false or unspecified:
+ // > ||||||||||||||||||||||||| (pause) |||||||||||||||||||||||||
+ // > X X
+ // >
+ // > Debounced with `at_begin` specified as true:
+ // > ||||||||||||||||||||||||| (pause) |||||||||||||||||||||||||
+ // > X X
+ //
+ // Usage:
+ //
+ // > var debounced = jQuery.debounce( delay, [ at_begin, ] callback );
+ // >
+ // > jQuery('selector').bind( 'someevent', debounced );
+ // > jQuery('selector').unbind( 'someevent', debounced );
+ //
+ // This also works in jQuery 1.4+:
+ //
+ // > jQuery('selector').bind( 'someevent', jQuery.debounce( delay, [ at_begin, ] callback ) );
+ // > jQuery('selector').unbind( 'someevent', callback );
+ //
+ // Arguments:
+ //
+ // delay - (Number) A zero-or-greater delay in milliseconds. For event
+ // callbacks, values around 100 or 250 (or even higher) are most useful.
+ // at_begin - (Boolean) Optional, defaults to false. If at_begin is false or
+ // unspecified, callback will only be executed `delay` milliseconds after
+ // the last debounced-function call. If at_begin is true, callback will be
+ // executed only at the first debounced-function call. (After the
+ // throttled-function has not been called for `delay` milliseconds, the
+ // internal counter is reset)
+ // callback - (Function) A function to be executed after delay milliseconds.
+ // The `this` context and all arguments are passed through, as-is, to
+ // `callback` when the debounced-function is executed.
+ //
+ // Returns:
+ //
+ // (Function) A new, debounced, function.
+
+ $.debounce = function( delay, at_begin, callback ) {
+ return callback === undefined
+ ? jq_throttle( delay, at_begin, false )
+ : jq_throttle( delay, callback, at_begin !== false );
+ };
+
+})(this);
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2019-03-14T00:52:20Z
+ * Date: 2019-03-20T23:07:02Z
*/
( function ( OO ) {
this.$element
.addClass( 'oo-ui-menuSectionOptionWidget' )
.removeAttr( 'role aria-selected' );
+ this.selected = false;
};
/* Setup */
* the first parameter and 'yes' or 'no' as the second.
* @param {Function} [options.handler] Callback to fire when the action is confirmed (user clicks
* the 'Yes' button).
+ * @param {string} [options.delegate] Optional selector used for jQuery event delegation
* @param {string} [options.i18n] Text to use for interface elements.
* @param {string} [options.i18n.space] Word separator to place between the three text messages.
* @param {string} [options.i18n.confirm] Text to use for the confirmation question.
$.fn.confirmable = function ( options ) {
options = $.extend( true, {}, $.fn.confirmable.defaultOptions, options || {} );
- return this.on( options.events, function ( e ) {
- var $element, $text, $buttonYes, $buttonNo, $wrapper, $interface, $elementClone,
- interfaceWidth, elementWidth, rtl, positionOffscreen, positionRestore, sideMargin;
+ if ( options.delegate === null ) {
+ return this.on( options.events, function ( e ) {
+ $.fn.confirmable.handler( e, options );
+ } );
+ }
- $element = $( this );
+ return this.on( options.events, options.delegate, function ( e ) {
+ $.fn.confirmable.handler( e, options );
+ } );
+ };
- if ( $element.data( 'jquery-confirmable-button' ) ) {
- // We're running on a clone of this element that represents the 'Yes' or 'No' button.
- // (This should never happen for the 'No' case unless calling code does bad things.)
- return;
- }
+ $.fn.confirmable.handler = function ( event, options ) {
+ var $element, $text, $buttonYes, $buttonNo, $wrapper, $interface, $elementClone,
+ interfaceWidth, elementWidth, rtl, positionOffscreen, positionRestore, sideMargin;
- // Only prevent native event handling. Stopping other JavaScript event handlers
- // is impossible because they might have already run (we have no control over the order).
- e.preventDefault();
+ $element = $( event.target );
- rtl = $element.css( 'direction' ) === 'rtl';
- if ( rtl ) {
- positionOffscreen = { position: 'absolute', right: '-9999px' };
- positionRestore = { position: '', right: '' };
- sideMargin = 'marginRight';
- } else {
- positionOffscreen = { position: 'absolute', left: '-9999px' };
- positionRestore = { position: '', left: '' };
- sideMargin = 'marginLeft';
- }
+ if ( $element.data( 'jquery-confirmable-button' ) ) {
+ // We're running on a clone of this element that represents the 'Yes' or 'No' button.
+ // (This should never happen for the 'No' case unless calling code does bad things.)
+ return;
+ }
- if ( $element.hasClass( 'jquery-confirmable-element' ) ) {
- $wrapper = $element.closest( '.jquery-confirmable-wrapper' );
- $interface = $wrapper.find( '.jquery-confirmable-interface' );
- $text = $interface.find( '.jquery-confirmable-text' );
- $buttonYes = $interface.find( '.jquery-confirmable-button-yes' );
- $buttonNo = $interface.find( '.jquery-confirmable-button-no' );
+ // Only prevent native event handling. Stopping other JavaScript event handlers
+ // is impossible because they might have already run (we have no control over the order).
+ event.preventDefault();
+
+ rtl = $element.css( 'direction' ) === 'rtl';
+ if ( rtl ) {
+ positionOffscreen = { position: 'absolute', right: '-9999px' };
+ positionRestore = { position: '', right: '' };
+ sideMargin = 'marginRight';
+ } else {
+ positionOffscreen = { position: 'absolute', left: '-9999px' };
+ positionRestore = { position: '', left: '' };
+ sideMargin = 'marginLeft';
+ }
- interfaceWidth = $interface.data( 'jquery-confirmable-width' );
- elementWidth = $element.data( 'jquery-confirmable-width' );
+ if ( $element.hasClass( 'jquery-confirmable-element' ) ) {
+ $wrapper = $element.closest( '.jquery-confirmable-wrapper' );
+ $interface = $wrapper.find( '.jquery-confirmable-interface' );
+ $text = $interface.find( '.jquery-confirmable-text' );
+ $buttonYes = $interface.find( '.jquery-confirmable-button-yes' );
+ $buttonNo = $interface.find( '.jquery-confirmable-button-no' );
+
+ interfaceWidth = $interface.data( 'jquery-confirmable-width' );
+ elementWidth = $element.data( 'jquery-confirmable-width' );
+ } else {
+ $elementClone = $element.clone( true );
+ $element.addClass( 'jquery-confirmable-element' );
+
+ elementWidth = $element.width();
+ $element.data( 'jquery-confirmable-width', elementWidth );
+
+ $wrapper = $( '<span>' )
+ .addClass( 'jquery-confirmable-wrapper' );
+ $element.wrap( $wrapper );
+
+ // Build the mini-dialog
+ $text = $( '<span>' )
+ .addClass( 'jquery-confirmable-text' )
+ .text( options.i18n.confirm );
+
+ // Clone original element along with event handlers to easily replicate its behavior.
+ // We could fiddle with .trigger() etc., but that is troublesome especially since
+ // Safari doesn't implement .click() on <a> links and jQuery follows suit.
+ $buttonYes = $elementClone.clone( true )
+ .addClass( 'jquery-confirmable-button jquery-confirmable-button-yes' )
+ .data( 'jquery-confirmable-button', true )
+ .text( options.i18n.yes );
+ if ( options.handler ) {
+ $buttonYes.on( options.events, options.handler );
+ }
+ if ( options.i18n.yesTitle ) {
+ $buttonYes.attr( 'title', options.i18n.yesTitle );
+ }
+ $buttonYes = options.buttonCallback( $buttonYes, 'yes' );
+
+ // Clone it without any events and prevent default action to represent the 'No' button.
+ $buttonNo = $elementClone.clone( false )
+ .addClass( 'jquery-confirmable-button jquery-confirmable-button-no' )
+ .data( 'jquery-confirmable-button', true )
+ .text( options.i18n.no )
+ .on( options.events, function ( e ) {
+ $element.css( sideMargin, 0 );
+ $interface.css( 'width', 0 );
+ e.preventDefault();
+ } );
+ if ( options.i18n.noTitle ) {
+ $buttonNo.attr( 'title', options.i18n.noTitle );
} else {
- $elementClone = $element.clone( true );
- $element.addClass( 'jquery-confirmable-element' );
-
- elementWidth = $element.width();
- $element.data( 'jquery-confirmable-width', elementWidth );
-
- $wrapper = $( '<span>' )
- .addClass( 'jquery-confirmable-wrapper' );
- $element.wrap( $wrapper );
-
- // Build the mini-dialog
- $text = $( '<span>' )
- .addClass( 'jquery-confirmable-text' )
- .text( options.i18n.confirm );
-
- // Clone original element along with event handlers to easily replicate its behavior.
- // We could fiddle with .trigger() etc., but that is troublesome especially since
- // Safari doesn't implement .click() on <a> links and jQuery follows suit.
- $buttonYes = $elementClone.clone( true )
- .addClass( 'jquery-confirmable-button jquery-confirmable-button-yes' )
- .data( 'jquery-confirmable-button', true )
- .text( options.i18n.yes );
- if ( options.handler ) {
- $buttonYes.on( options.events, options.handler );
- }
- if ( options.i18n.yesTitle ) {
- $buttonYes.attr( 'title', options.i18n.yesTitle );
- }
- $buttonYes = options.buttonCallback( $buttonYes, 'yes' );
-
- // Clone it without any events and prevent default action to represent the 'No' button.
- $buttonNo = $elementClone.clone( false )
- .addClass( 'jquery-confirmable-button jquery-confirmable-button-no' )
- .data( 'jquery-confirmable-button', true )
- .text( options.i18n.no )
- .on( options.events, function ( e ) {
- $element.css( sideMargin, 0 );
- $interface.css( 'width', 0 );
- e.preventDefault();
- } );
- if ( options.i18n.noTitle ) {
- $buttonNo.attr( 'title', options.i18n.noTitle );
- } else {
- $buttonNo.removeAttr( 'title' );
- }
- $buttonNo = options.buttonCallback( $buttonNo, 'no' );
-
- // Prevent memory leaks
- $elementClone.remove();
-
- $interface = $( '<span>' )
- .addClass( 'jquery-confirmable-interface' )
- .append( $text, options.i18n.space, $buttonYes, options.i18n.space, $buttonNo );
- $interface = options.wrapperCallback( $interface );
-
- // Render offscreen to measure real width
- $interface.css( positionOffscreen );
- // Insert it in the correct place while we're at it
- $element.after( $interface );
- interfaceWidth = $interface.width();
- $interface.data( 'jquery-confirmable-width', interfaceWidth );
- $interface.css( positionRestore );
-
- // Hide to animate the transition later
- $interface.css( 'width', 0 );
+ $buttonNo.removeAttr( 'title' );
}
+ $buttonNo = options.buttonCallback( $buttonNo, 'no' );
+
+ // Prevent memory leaks
+ $elementClone.remove();
+
+ $interface = $( '<span>' )
+ .addClass( 'jquery-confirmable-interface' )
+ .append( $text, options.i18n.space, $buttonYes, options.i18n.space, $buttonNo );
+ $interface = options.wrapperCallback( $interface );
+
+ // Render offscreen to measure real width
+ $interface.css( positionOffscreen );
+ // Insert it in the correct place while we're at it
+ $element.after( $interface );
+ interfaceWidth = $interface.width();
+ $interface.data( 'jquery-confirmable-width', interfaceWidth );
+ $interface.css( positionRestore );
+
+ // Hide to animate the transition later
+ $interface.css( 'width', 0 );
+ }
- // Hide element, show interface. This triggers both transitions.
- // In a timeout to trigger the 'width' transition.
- setTimeout( function () {
- $element.css( sideMargin, -elementWidth );
- $interface.css( 'width', interfaceWidth );
- }, 1 );
- } );
+ // Hide element, show interface. This triggers both transitions.
+ // In a timeout to trigger the 'width' transition.
+ setTimeout( function () {
+ $element.css( sideMargin, -elementWidth );
+ $interface.css( 'width', interfaceWidth );
+ }, 1 );
};
/**
wrapperCallback: identity,
buttonCallback: identity,
handler: null,
+ delegate: null,
i18n: {
space: ' ',
confirm: 'Are you sure?',
--- /dev/null
+/*!
+ * JavaScript for rollback confirmation prompt
+ */
+( function () {
+
+ var postRollback = function ( url ) {
+ var $form = $( '<form>', {
+ action: url,
+ method: 'post'
+ } );
+ $form.appendTo( 'body' ).trigger( 'submit' );
+ };
+
+ $( '.mw-rollback-link a' ).each( function () {
+ $( this ).confirmable( {
+ i18n: {
+ confirm: mw.msg( 'rollback-confirmation-confirm', $( this ).data( 'rollback-count' ) ),
+ yes: mw.msg( 'rollback-confirmation-yes' ),
+ no: mw.msg( 'rollback-confirmation-no' )
+ },
+ handler: function ( e ) {
+ e.preventDefault();
+ postRollback( $( this ).attr( 'href' ) );
+ }
+ } );
+ } );
+
+}() );
pageRestrictionsWidget = infuseIfExists( $( '#mw-input-wpPageRestrictions' ) ),
namespaceRestrictionsWidget = infuseIfExists( $( '#mw-input-wpNamespaceRestrictions' ) ),
createAccountWidget = infuseIfExists( $( '#mw-input-wpCreateAccount' ) ),
- userChangedCreateAccount = false,
+ userChangedCreateAccount = $( '#mw-input-wpBlockId' ).val() || $( '#mw-input-wpWasPosted' ).val() || false,
updatingBlockOptions = false;
function updateBlockOptions() {
'GenericArrayObjectTest' => "$testDir/phpunit/includes/libs/GenericArrayObjectTest.php",
# tests/phpunit/maintenance
+ 'MediaWiki\Tests\Maintenance\DumpAsserter' => "$testDir/phpunit/maintenance/DumpAsserter.php",
'MediaWiki\Tests\Maintenance\DumpTestCase' => "$testDir/phpunit/maintenance/DumpTestCase.php",
'MediaWiki\Tests\Maintenance\MaintenanceBaseTestCase' => "$testDir/phpunit/maintenance/MaintenanceBaseTestCase.php",
// ->disallowMockingUnknownTypes()
->getMock();
}
+
+ /**
+ * Marks the current test as risky. This
+ * is a forward port of the markAsRisky function that
+ * was introduced in PHPUnit 5.7.6.
+ */
+ public function markAsRisky() {
+ if ( is_callable( 'parent::markAsRisky' ) ) {
+ return parent::markAsRisky();
+ }
+
+ // "risky" tests are not supported in phpunit 4, so just ignore
+ }
+
}
);
}
+ /**
+ * @covers Linker::generateRollback
+ * @dataProvider provideCasesForRollbackGeneration
+ */
+ public function testGenerateRollback( $rollbackEnabled, $expectedModules ) {
+ $this->markTestSkippedIfDbType( 'postgres' );
+
+ $context = RequestContext::getMain();
+ $user = $context->getUser();
+ $user->setOption( 'showrollbackconfirmation', $rollbackEnabled );
+
+ $pageData = $this->insertPage( 'Rollback_Test_Page' );
+ $page = WikiPage::factory( $pageData['title'] );
+
+ $updater = $page->newPageUpdater( $user );
+ $updater->setContent( \MediaWiki\Revision\SlotRecord::MAIN,
+ new TextContent( 'Technical Wishes 123!' )
+ );
+ $summary = CommentStoreComment::newUnsavedComment( 'Some comment!' );
+ $updater->saveRevision( $summary );
+
+ $rollbackOutput = Linker::generateRollback( $page->getRevision(), $context );
+ $modules = $context->getOutput()->getModules();
+
+ $this->assertEquals( $expectedModules, $modules );
+ $this->assertContains( 'rollback 1 edit', $rollbackOutput );
+ }
+
+ public static function provideCasesForRollbackGeneration() {
+ return [
+ [
+ true,
+ [ 'mediawiki.page.rollback.confirmation' ]
+
+ ],
+ [
+ false,
+ []
+ ]
+ ];
+ }
+
public static function provideCasesForFormatLinksInComment() {
// phpcs:disable Generic.Files.LineLength
return [
* @covers MediaWiki\Preferences\DefaultPreferencesFactory::renderingPreferences()
*/
public function testShowRollbackConfIsHiddenForUsersWithoutRollbackRights() {
- // TODO Remove temporary skip marker once feature is added back in
- $this->markTestSkipped();
$userMock = $this->getMockBuilder( User::class )
->disableOriginalConstructor()
->getMock();
* @covers MediaWiki\Preferences\DefaultPreferencesFactory::renderingPreferences()
*/
public function testShowRollbackConfIsShownForUsersWithRollbackRights() {
- // TODO Remove temporary skip marker once feature is added back in
- $this->markTestSkipped();
$userMock = $this->getMockBuilder( User::class )
->disableOriginalConstructor()
->getMock();
<?php
use MediaWiki\MediaWikiServices;
+use Wikimedia\TestingAccessWrapper;
/**
* @author Addshore
// setNotificationTimestampsForUser specifying a title
$this->assertTrue(
- $store->setNotificationTimestampsForUser( $user, '20200202020202', [ $title ] )
+ $store->setNotificationTimestampsForUser( $user, '20100202020202', [ $title ] )
);
$this->assertEquals(
- '20200202020202',
+ '20100202020202',
$store->getWatchedItem( $user, $title )->getNotificationTimestamp()
);
// setNotificationTimestampsForUser not specifying a title
+ // This will try to use a DeferredUpdate; disable that
+ $mockCallback = function ( $callback ) {
+ $callback();
+ };
+ $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback );
$this->assertTrue(
- $store->setNotificationTimestampsForUser( $user, '20210202020202' )
+ $store->setNotificationTimestampsForUser( $user, '20110202020202' )
);
+ // Because the operation above is normally deferred, it doesn't clear the cache
+ // Clear the cache manually
+ $wrappedStore = TestingAccessWrapper::newFromObject( $store );
+ $wrappedStore->uncacheUser( $user );
$this->assertEquals(
- '20210202020202',
+ '20110202020202',
$store->getWatchedItem( $user, $title )->getNotificationTimestamp()
);
}
$mock->expects( $this->any() )
->method( 'getId' )
->will( $this->returnValue( $id ) );
+ $mock->expects( $this->any() )
+ ->method( 'getUserPage' )
+ ->will( $this->returnValue( Title::makeTitle( NS_USER, 'MockUser' ) ) );
return $mock;
}
$user = $this->getMockNonAnonUserWithId( 1 );
$timestamp = '20100101010101';
- $mockDb = $this->getMockDb();
- $mockDb->expects( $this->once() )
- ->method( 'update' )
- ->with(
- 'watchlist',
- [ 'wl_notificationtimestamp' => 'TS' . $timestamp . 'TS' ],
- [ 'wl_user' => 1 ]
- )
- ->will( $this->returnValue( true ) );
- $mockDb->expects( $this->exactly( 1 ) )
- ->method( 'timestamp' )
- ->will( $this->returnCallback( function ( $value ) {
- return 'TS' . $value . 'TS';
- } ) );
-
$store = $this->newWatchedItemStore(
- $this->getMockLBFactory( $mockDb ),
+ $this->getMockLBFactory( $this->getMockDb() ),
$this->getMockJobQueueGroup(),
$this->getMockCache(),
$this->getMockReadOnlyMode()
);
+ // Note: This does not actually assert the job is correct
+ $callableCallCounter = 0;
+ $mockCallback = function ( $callable ) use ( &$callableCallCounter ) {
+ $callableCallCounter++;
+ $this->assertInternalType( 'callable', $callable );
+ };
+ $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback );
+
$this->assertTrue(
$store->setNotificationTimestampsForUser( $user, $timestamp )
);
+ $this->assertEquals( 1, $callableCallCounter );
}
public function testSetNotificationTimestampsForUser_nullTimestamp() {
$user = $this->getMockNonAnonUserWithId( 1 );
$timestamp = null;
- $mockDb = $this->getMockDb();
- $mockDb->expects( $this->once() )
- ->method( 'update' )
- ->with(
- 'watchlist',
- [ 'wl_notificationtimestamp' => null ],
- [ 'wl_user' => 1 ]
- )
- ->will( $this->returnValue( true ) );
- $mockDb->expects( $this->exactly( 0 ) )
- ->method( 'timestamp' )
- ->will( $this->returnCallback( function ( $value ) {
- return 'TS' . $value . 'TS';
- } ) );
-
$store = $this->newWatchedItemStore(
- $this->getMockLBFactory( $mockDb ),
+ $this->getMockLBFactory( $this->getMockDb() ),
$this->getMockJobQueueGroup(),
$this->getMockCache(),
$this->getMockReadOnlyMode()
);
+ // Note: This does not actually assert the job is correct
+ $callableCallCounter = 0;
+ $mockCallback = function ( $callable ) use ( &$callableCallCounter ) {
+ $callableCallCounter++;
+ $this->assertInternalType( 'callable', $callable );
+ };
+ $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback );
+
$this->assertTrue(
$store->setNotificationTimestampsForUser( $user, $timestamp )
);
->with(
'watchlist',
[ 'wl_notificationtimestamp' => 'TS' . $timestamp . 'TS' ],
- [ 'wl_user' => 1, 0 => 'makeWhereFrom2d return value' ]
+ [ 'wl_user' => 1, 'wl_namespace' => 0, 'wl_title' => [ 'Foo', 'Bar' ] ]
)
->will( $this->returnValue( true ) );
$mockDb->expects( $this->exactly( 1 ) )
return 'TS' . $value . 'TS';
} ) );
$mockDb->expects( $this->once() )
- ->method( 'makeWhereFrom2d' )
- ->with(
- [ [ 'Foo' => 1, 'Bar' => 1 ] ],
- $this->isType( 'string' ),
- $this->isType( 'string' )
- )
- ->will( $this->returnValue( 'makeWhereFrom2d return value' ) );
+ ->method( 'affectedRows' )
+ ->will( $this->returnValue( 2 ) );
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
--- /dev/null
+<?php
+
+namespace MediaWiki\Tests\Maintenance;
+
+use PHPUnit\Framework\Assert;
+use XMLReader;
+
+/**
+ * Helper for asserting the structure of an XML dump stream.
+ */
+class DumpAsserter {
+
+ /**
+ * Holds the XMLReader used for analyzing an XML dump
+ *
+ * @var XMLReader|null
+ */
+ protected $xml = null;
+
+ /**
+ * XML dump schema version
+ *
+ * @var string
+ */
+ protected $schemaVersion;
+
+ /**
+ * DumpAsserts constructor.
+ *
+ * @param string $schemaVersion see XML_DUMP_SCHEMA_VERSION_XX
+ */
+ public function __construct( $schemaVersion ) {
+ $this->schemaVersion = $schemaVersion;
+ }
+
+ /**
+ * Step the current XML reader until node end of given name is found.
+ *
+ * @param string $name Name of the closing element to look for
+ * (e.g.: "mediawiki" when looking for </mediawiki>)
+ *
+ * @return bool True if the end node could be found. false otherwise.
+ */
+ public function skipToNodeEnd( $name ) {
+ while ( $this->xml->read() ) {
+ if ( $this->xml->nodeType == XMLReader::END_ELEMENT &&
+ $this->xml->name == $name
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Step the current XML reader to the first element start after the node
+ * end of a given name.
+ *
+ * @param string $name Name of the closing element to look for
+ * (e.g.: "mediawiki" when looking for </mediawiki>)
+ *
+ * @return bool True if new element after the closing of $name could be
+ * found. false otherwise.
+ */
+ public function skipPastNodeEnd( $name ) {
+ Assert::assertTrue( $this->skipToNodeEnd( $name ),
+ "Skipping to end of $name" );
+ while ( $this->xml->read() ) {
+ if ( $this->xml->nodeType == XMLReader::ELEMENT ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Opens an XML file to analyze and optionally skips past siteinfo.
+ *
+ * @param string $fname Name of file to analyze
+ * @param bool $skip_siteinfo (optional) If true, step the xml reader
+ * to the first element after </siteinfo>
+ */
+ public function assertDumpStart( $fname, $skip_siteinfo = true ) {
+ $this->xml = new XMLReader();
+
+ Assert::assertTrue( $this->xml->open( $fname ),
+ "Opening temporary file $fname via XMLReader failed" );
+ if ( $skip_siteinfo ) {
+ Assert::assertTrue( $this->skipPastNodeEnd( "siteinfo" ),
+ "Skipping past end of siteinfo" );
+ }
+ }
+
+ /**
+ * Asserts that the xml reader is at the final closing tag of an xml file and
+ * closes the reader.
+ *
+ * @param string $name (optional) the name of the final tag
+ * (e.g.: "mediawiki" for </mediawiki>)
+ */
+ public function assertDumpEnd( $name = "mediawiki" ) {
+ $this->assertNodeEnd( $name, false );
+ if ( $this->xml->read() ) {
+ $this->skipWhitespace();
+ }
+ Assert::assertEquals( $this->xml->nodeType, XMLReader::NONE,
+ "No proper entity left to parse" );
+ $this->xml->close();
+ }
+
+ /**
+ * Steps the xml reader over white space
+ */
+ public function skipWhitespace() {
+ $cont = true;
+ while ( $cont && ( ( $this->xml->nodeType == XMLReader::WHITESPACE )
+ || ( $this->xml->nodeType == XMLReader::SIGNIFICANT_WHITESPACE ) ) ) {
+ $cont = $this->xml->read();
+ }
+ }
+
+ /**
+ * Asserts that the xml reader is at an element of given name, and optionally
+ * skips past it.
+ *
+ * @param string $name The name of the element to check for
+ * (e.g.: "mediawiki" for <mediawiki>)
+ * @param bool $skip (optional) if true, skip past the found element
+ */
+ public function assertNodeStart( $name, $skip = true ) {
+ Assert::assertEquals( $name, $this->xml->name, "Node name" );
+ Assert::assertEquals( XMLReader::ELEMENT, $this->xml->nodeType, "Node type" );
+ if ( $skip ) {
+ Assert::assertTrue( $this->xml->read(), "Skipping past start tag" );
+ }
+ }
+
+ /**
+ * Asserts that the xml reader is at an closing element of given name, and optionally
+ * skips past it.
+ *
+ * @param string $name The name of the closing element to check for
+ * (e.g.: "mediawiki" for </mediawiki>)
+ * @param bool $skip (optional) if true, skip past the found element
+ */
+ public function assertNodeEnd( $name, $skip = true ) {
+ Assert::assertEquals( $name, $this->xml->name, "Node name" );
+ Assert::assertEquals( XMLReader::END_ELEMENT, $this->xml->nodeType, "Node type" );
+ if ( $skip ) {
+ Assert::assertTrue( $this->xml->read(), "Skipping past end tag" );
+ }
+ }
+
+ /**
+ * Asserts that the xml reader is at an element of given tag that contains a given text,
+ * and skips over the element.
+ *
+ * @param string $name The name of the element to check for
+ * (e.g.: "mediawiki" for <mediawiki>...</mediawiki>)
+ * @param string|bool $text If string, check if it equals the elements text.
+ * If false, ignore the element's text
+ * @param bool $skip_ws (optional) if true, skip past white spaces that trail the
+ * closing element.
+ */
+ public function assertTextNode( $name, $text, $skip_ws = true ) {
+ $this->assertNodeStart( $name );
+
+ if ( $text !== false ) {
+ Assert::assertEquals( $text, $this->xml->value, "Text of node " . $name );
+ }
+ Assert::assertTrue( $this->xml->read(), "Skipping past processed text of " . $name );
+ $this->assertNodeEnd( $name );
+
+ if ( $skip_ws ) {
+ $this->skipWhitespace();
+ }
+ }
+
+ /**
+ * Asserts that the xml reader is at the start of a page element and skips over the first
+ * tags, after checking them.
+ *
+ * Besides the opening page element, this function also checks for and skips over the
+ * title, ns, and id tags. Hence after this function, the xml reader is at the first
+ * revision of the current page.
+ *
+ * @param int $id Id of the page to assert
+ * @param int $ns Number of namespage to assert
+ * @param string $name Title of the current page
+ */
+ public function assertPageStart( $id, $ns, $name ) {
+ $this->assertNodeStart( "page" );
+ $this->skipWhitespace();
+
+ $this->assertTextNode( "title", $name );
+ $this->assertTextNode( "ns", $ns );
+ $this->assertTextNode( "id", $id );
+ }
+
+ /**
+ * Asserts that the xml reader is at the page's closing element and skips to the next
+ * element.
+ */
+ public function assertPageEnd() {
+ $this->assertNodeEnd( "page" );
+ $this->skipWhitespace();
+ }
+
+ /**
+ * Asserts that the xml reader is at a revision and checks its representation before
+ * skipping over it.
+ *
+ * @param int $id Id of the revision
+ * @param string $summary Summary of the revision
+ * @param int $text_id Id of the revision's text
+ * @param int $text_bytes Number of bytes in the revision's text
+ * @param string $text_sha1 The base36 SHA-1 of the revision's text
+ * @param string|bool $text (optional) The revision's string, or false to check for a
+ * revision stub
+ * @param int|bool $parentid (optional) id of the parent revision
+ * @param string $model The expected content model id (default: CONTENT_MODEL_WIKITEXT)
+ * @param string $format The expected format model id (default: CONTENT_FORMAT_WIKITEXT)
+ */
+ public function assertRevision( $id, $summary, $text_id, $text_bytes,
+ $text_sha1, $text = false, $parentid = false,
+ $model = CONTENT_MODEL_WIKITEXT, $format = CONTENT_FORMAT_WIKITEXT
+ ) {
+ $this->assertNodeStart( "revision" );
+ $this->skipWhitespace();
+
+ $this->assertTextNode( "id", $id );
+ if ( $parentid !== false ) {
+ $this->assertTextNode( "parentid", $parentid );
+ }
+ $this->assertTextNode( "timestamp", false );
+
+ $this->assertNodeStart( "contributor" );
+ $this->skipWhitespace();
+ $this->assertTextNode( "ip", false );
+ $this->assertNodeEnd( "contributor" );
+ $this->skipWhitespace();
+
+ $this->assertTextNode( "comment", $summary );
+ $this->skipWhitespace();
+
+ $this->assertTextNode( "model", $model );
+ $this->skipWhitespace();
+
+ $this->assertTextNode( "format", $format );
+ $this->skipWhitespace();
+
+ if ( $this->xml->name == "text" ) {
+ // note: <text> tag may occur here or at the very end.
+ $text_found = true;
+ $this->assertText( $id, $text_id, $text_bytes, $text );
+ } else {
+ $text_found = false;
+ }
+
+ $this->assertTextNode( "sha1", $text_sha1 );
+
+ if ( !$text_found ) {
+ $this->assertText( $id, $text_id, $text_bytes, $text );
+ }
+
+ $this->assertNodeEnd( "revision" );
+ $this->skipWhitespace();
+ }
+
+ public function assertText( $id, $text_id, $text_bytes, $text ) {
+ $this->assertNodeStart( "text", false );
+ if ( $text_bytes !== false ) {
+ Assert::assertEquals( $this->xml->getAttribute( "bytes" ), $text_bytes,
+ "Attribute 'bytes' of revision " . $id );
+ }
+
+ if ( $text === false ) {
+ // Testing for a stub
+ Assert::assertEquals( $this->xml->getAttribute( "id" ), $text_id,
+ "Text id of revision " . $id );
+ Assert::assertFalse( $this->xml->hasValue, "Revision has text" );
+ Assert::assertTrue( $this->xml->read(), "Skipping text start tag" );
+ if ( ( $this->xml->nodeType == XMLReader::END_ELEMENT )
+ && ( $this->xml->name == "text" )
+ ) {
+ $this->xml->read();
+ }
+ $this->skipWhitespace();
+ } else {
+ // Testing for a real dump
+ Assert::assertTrue( $this->xml->read(), "Skipping text start tag" );
+ Assert::assertEquals( $text, $this->xml->value, "Text of revision " . $id );
+ Assert::assertTrue( $this->xml->read(), "Skipping past text" );
+ $this->assertNodeEnd( "text" );
+ $this->skipWhitespace();
+ }
+ }
+
+ /**
+ * asserts that the xml reader is at the beginning of a log entry and skips over
+ * it while analyzing it.
+ *
+ * @param int $id Id of the log entry
+ * @param string $user_name User name of the log entry's performer
+ * @param int $user_id User id of the log entry 's performer
+ * @param string|null $comment Comment of the log entry. If null, the comment text is ignored.
+ * @param string $type Type of the log entry
+ * @param string $subtype Subtype of the log entry
+ * @param string $title Title of the log entry's target
+ * @param array $parameters (optional) unserialized data accompanying the log entry
+ */
+ public function assertLogItem( $id, $user_name, $user_id, $comment, $type,
+ $subtype, $title, $parameters = []
+ ) {
+ $this->assertNodeStart( "logitem" );
+ $this->skipWhitespace();
+
+ $this->assertTextNode( "id", $id );
+ $this->assertTextNode( "timestamp", false );
+
+ $this->assertNodeStart( "contributor" );
+ $this->skipWhitespace();
+ $this->assertTextNode( "username", $user_name );
+ $this->assertTextNode( "id", $user_id );
+ $this->assertNodeEnd( "contributor" );
+ $this->skipWhitespace();
+
+ if ( $comment !== null ) {
+ $this->assertTextNode( "comment", $comment );
+ }
+ $this->assertTextNode( "type", $type );
+ $this->assertTextNode( "action", $subtype );
+ $this->assertTextNode( "logtitle", $title );
+
+ $this->assertNodeStart( "params" );
+ $parameters_xml = unserialize( $this->xml->value );
+ Assert::assertEquals( $parameters, $parameters_xml );
+ Assert::assertTrue( $this->xml->read(), "Skipping past processed text of params" );
+ $this->assertNodeEnd( "params" );
+ $this->skipWhitespace();
+
+ $this->assertNodeEnd( "logitem" );
+ $this->skipWhitespace();
+ }
+}
namespace MediaWiki\Tests\Maintenance;
use ContentHandler;
+use DOMDocument;
use ExecutableFinder;
use MediaWikiLangTestCase;
-use Page;
use User;
-use XMLReader;
+use WikiExporter;
+use WikiPage;
use MWException;
/**
*/
protected $exceptionFromAddDBData = null;
- /**
- * Holds the XMLReader used for analyzing an XML dump
- *
- * @var XMLReader|null
- */
- protected $xml = null;
-
/** @var bool|null Whether the 'gzip' utility is available */
protected static $hasGzip = null;
/**
* Adds a revision to a page, while returning the resuting revision's id
*
- * @param Page $page Page to add the revision to
+ * @param WikiPage $page Page to add the revision to
* @param string $text Revisions text
* @param string $summary Revisions summary
* @param string $model The model ID (defaults to wikitext)
* @throws MWException
* @return array
*/
- protected function addRevision( Page $page, $text, $summary, $model = CONTENT_MODEL_WIKITEXT ) {
+ protected function addRevision(
+ WikiPage $page,
+ $text,
+ $summary,
+ $model = CONTENT_MODEL_WIKITEXT
+ ) {
$status = $page->doEditContent(
ContentHandler::makeContent( $text, $page->getTitle(), $model ),
$summary
);
}
+ public static function setUpBeforeClass() {
+ parent::setUpBeforeClass();
+
+ if ( !function_exists( 'libxml_set_external_entity_loader' ) ) {
+ return;
+ }
+
+ // The W3C is intentionally slow about returning schema files,
+ // see <https://www.w3.org/Help/Webmaster#slowdtd>.
+ // To work around that, we keep our own copies of the relevant schema files.
+ libxml_set_external_entity_loader(
+ function ( $public, $system, $context ) {
+ switch ( $system ) {
+ // if more schema files are needed, add them here.
+ case 'http://www.w3.org/2001/xml.xsd':
+ $file = __DIR__ . '/xml.xsd';
+ break;
+ default:
+ if ( is_file( $system ) ) {
+ $file = $system;
+ } else {
+ return null;
+ }
+ }
+
+ return $file;
+ }
+ );
+ }
+
/**
* Default set up function.
*
$this->setMwGlobals( 'wgUser', new User() );
}
+ /**
+ * Returns the path to the XML schema file for the given schema version.
+ *
+ * @param string|null $schemaVersion
+ *
+ * @return string
+ */
+ protected function getXmlSchemaPath( $schemaVersion = null ) {
+ global $IP, $wgXmlDumpSchemaVersion;
+
+ $schemaVersion = $schemaVersion ?: $wgXmlDumpSchemaVersion;
+
+ return "$IP/docs/export-$schemaVersion.xsd";
+ }
+
/**
* Checks for test output consisting only of lines containing ETA announcements
*/
}
/**
- * Step the current XML reader until node end of given name is found.
- *
- * @param string $name Name of the closing element to look for
- * (e.g.: "mediawiki" when looking for </mediawiki>)
+ * @param null|string $schemaVersion
*
- * @return bool True if the end node could be found. false otherwise.
+ * @return DumpAsserter
*/
- protected function skipToNodeEnd( $name ) {
- while ( $this->xml->read() ) {
- if ( $this->xml->nodeType == XMLReader::END_ELEMENT &&
- $this->xml->name == $name
- ) {
- return true;
- }
- }
-
- return false;
+ protected function getDumpAsserter( $schemaVersion = null ) {
+ $schemaVersion = $schemaVersion ?: WikiExporter::schemaVersion();
+ return new DumpAsserter( $schemaVersion );
}
/**
- * Step the current XML reader to the first element start after the node
- * end of a given name.
- *
- * @param string $name Name of the closing element to look for
- * (e.g.: "mediawiki" when looking for </mediawiki>)
- *
- * @return bool True if new element after the closing of $name could be
- * found. false otherwise.
+ * Checks an XML file against an XSD schema.
*/
- protected function skipPastNodeEnd( $name ) {
- $this->assertTrue( $this->skipToNodeEnd( $name ),
- "Skipping to end of $name" );
- while ( $this->xml->read() ) {
- if ( $this->xml->nodeType == XMLReader::ELEMENT ) {
- return true;
- }
+ protected function assertDumpSchema( $fname, $schemaFile ) {
+ if ( !function_exists( 'libxml_use_internal_errors' ) ) {
+ // Would be nice to leave a warning somehow.
+ // We don't want to skip all of the test case that calls this, though.
+ $this->markAsRisky();
+ return;
}
-
- return false;
- }
-
- /**
- * Opens an XML file to analyze and optionally skips past siteinfo.
- *
- * @param string $fname Name of file to analyze
- * @param bool $skip_siteinfo (optional) If true, step the xml reader
- * to the first element after </siteinfo>
- */
- protected function assertDumpStart( $fname, $skip_siteinfo = true ) {
- $this->xml = new XMLReader();
- $this->assertTrue( $this->xml->open( $fname ),
- "Opening temporary file $fname via XMLReader failed" );
- if ( $skip_siteinfo ) {
- $this->assertTrue( $this->skipPastNodeEnd( "siteinfo" ),
- "Skipping past end of siteinfo" );
+ if ( defined( 'HHVM_VERSION' ) ) {
+ // In HHVM, loading a schema from a file is disabled per default.
+ // This is controlled by hhvm.libxml.ext_entity_whitelist which
+ // cannot be read with ini_get(), see
+ // <https://docs.hhvm.com/hhvm/configuration/INI-settings#xml>.
+ // Would be nice to leave a warning somehow.
+ // We don't want to skip all of the test case that calls this, though.
+ $this->markAsRisky();
+ return;
}
- }
- /**
- * Asserts that the xml reader is at the final closing tag of an xml file and
- * closes the reader.
- *
- * @param string $name (optional) the name of the final tag
- * (e.g.: "mediawiki" for </mediawiki>)
- */
- protected function assertDumpEnd( $name = "mediawiki" ) {
- $this->assertNodeEnd( $name, false );
- if ( $this->xml->read() ) {
- $this->skipWhitespace();
- }
- $this->assertEquals( $this->xml->nodeType, XMLReader::NONE,
- "No proper entity left to parse" );
- $this->xml->close();
- }
+ $xml = new DOMDocument();
+ $this->assertTrue( $xml->load( $fname ),
+ "Opening temporary file $fname via DOMDocument failed" );
- /**
- * Steps the xml reader over white space
- */
- protected function skipWhitespace() {
- $cont = true;
- while ( $cont && ( ( $this->xml->nodeType == XMLReader::WHITESPACE )
- || ( $this->xml->nodeType == XMLReader::SIGNIFICANT_WHITESPACE ) ) ) {
- $cont = $this->xml->read();
- }
- }
+ // Don't throw
+ $oldLibXmlInternalErrors = libxml_use_internal_errors( true );
- /**
- * Asserts that the xml reader is at an element of given name, and optionally
- * skips past it.
- *
- * @param string $name The name of the element to check for
- * (e.g.: "mediawiki" for <mediawiki>)
- * @param bool $skip (optional) if true, skip past the found element
- */
- protected function assertNodeStart( $name, $skip = true ) {
- $this->assertEquals( $name, $this->xml->name, "Node name" );
- $this->assertEquals( XMLReader::ELEMENT, $this->xml->nodeType, "Node type" );
- if ( $skip ) {
- $this->assertTrue( $this->xml->read(), "Skipping past start tag" );
- }
- }
-
- /**
- * Asserts that the xml reader is at an closing element of given name, and optionally
- * skips past it.
- *
- * @param string $name The name of the closing element to check for
- * (e.g.: "mediawiki" for </mediawiki>)
- * @param bool $skip (optional) if true, skip past the found element
- */
- protected function assertNodeEnd( $name, $skip = true ) {
- $this->assertEquals( $name, $this->xml->name, "Node name" );
- $this->assertEquals( XMLReader::END_ELEMENT, $this->xml->nodeType, "Node type" );
- if ( $skip ) {
- $this->assertTrue( $this->xml->read(), "Skipping past end tag" );
- }
- }
-
- /**
- * Asserts that the xml reader is at an element of given tag that contains a given text,
- * and skips over the element.
- *
- * @param string $name The name of the element to check for
- * (e.g.: "mediawiki" for <mediawiki>...</mediawiki>)
- * @param string|bool $text If string, check if it equals the elements text.
- * If false, ignore the element's text
- * @param bool $skip_ws (optional) if true, skip past white spaces that trail the
- * closing element.
- */
- protected function assertTextNode( $name, $text, $skip_ws = true ) {
- $this->assertNodeStart( $name );
-
- if ( $text !== false ) {
- $this->assertEquals( $text, $this->xml->value, "Text of node " . $name );
- }
- $this->assertTrue( $this->xml->read(), "Skipping past processed text of " . $name );
- $this->assertNodeEnd( $name );
-
- if ( $skip_ws ) {
- $this->skipWhitespace();
- }
- }
-
- /**
- * Asserts that the xml reader is at the start of a page element and skips over the first
- * tags, after checking them.
- *
- * Besides the opening page element, this function also checks for and skips over the
- * title, ns, and id tags. Hence after this function, the xml reader is at the first
- * revision of the current page.
- *
- * @param int $id Id of the page to assert
- * @param int $ns Number of namespage to assert
- * @param string $name Title of the current page
- */
- protected function assertPageStart( $id, $ns, $name ) {
- $this->assertNodeStart( "page" );
- $this->skipWhitespace();
-
- $this->assertTextNode( "title", $name );
- $this->assertTextNode( "ns", $ns );
- $this->assertTextNode( "id", $id );
- }
+ // NOTE: if this reports "Invalid Schema", the schema may be referencing an external
+ // entity (typically, another schema) that needs to be mapped in the
+ // libxml_set_external_entity_loader callback defined in setUpBeforeClass() above!
+ // Or $schemaFile doesn't point to a schema file, or the schema is indeed just broken.
+ if ( !$xml->schemaValidate( $schemaFile ) ) {
+ $errorText = '';
- /**
- * Asserts that the xml reader is at the page's closing element and skips to the next
- * element.
- */
- protected function assertPageEnd() {
- $this->assertNodeEnd( "page" );
- $this->skipWhitespace();
- }
-
- /**
- * Asserts that the xml reader is at a revision and checks its representation before
- * skipping over it.
- *
- * @param int $id Id of the revision
- * @param string $summary Summary of the revision
- * @param int $text_id Id of the revision's text
- * @param int $text_bytes Number of bytes in the revision's text
- * @param string $text_sha1 The base36 SHA-1 of the revision's text
- * @param string|bool $text (optional) The revision's string, or false to check for a
- * revision stub
- * @param int|bool $parentid (optional) id of the parent revision
- * @param string $model The expected content model id (default: CONTENT_MODEL_WIKITEXT)
- * @param string $format The expected format model id (default: CONTENT_FORMAT_WIKITEXT)
- */
- protected function assertRevision( $id, $summary, $text_id, $text_bytes,
- $text_sha1, $text = false, $parentid = false,
- $model = CONTENT_MODEL_WIKITEXT, $format = CONTENT_FORMAT_WIKITEXT
- ) {
- $this->assertNodeStart( "revision" );
- $this->skipWhitespace();
-
- $this->assertTextNode( "id", $id );
- if ( $parentid !== false ) {
- $this->assertTextNode( "parentid", $parentid );
- }
- $this->assertTextNode( "timestamp", false );
-
- $this->assertNodeStart( "contributor" );
- $this->skipWhitespace();
- $this->assertTextNode( "ip", false );
- $this->assertNodeEnd( "contributor" );
- $this->skipWhitespace();
-
- $this->assertTextNode( "comment", $summary );
- $this->skipWhitespace();
-
- $this->assertTextNode( "model", $model );
- $this->skipWhitespace();
-
- $this->assertTextNode( "format", $format );
- $this->skipWhitespace();
-
- if ( $this->xml->name == "text" ) {
- // note: <text> tag may occur here or at the very end.
- $text_found = true;
- $this->assertText( $id, $text_id, $text_bytes, $text );
- } else {
- $text_found = false;
- }
+ foreach ( libxml_get_errors() as $error ) {
+ $errorText .= "\nline {$error->line}: {$error->message}";
+ }
- $this->assertTextNode( "sha1", $text_sha1 );
+ libxml_clear_errors();
- if ( !$text_found ) {
- $this->assertText( $id, $text_id, $text_bytes, $text );
+ $this->fail(
+ "Failed asserting that $fname conforms to the schema in $schemaFile:\n$errorText"
+ );
}
- $this->assertNodeEnd( "revision" );
- $this->skipWhitespace();
+ libxml_use_internal_errors( $oldLibXmlInternalErrors );
}
- protected function assertText( $id, $text_id, $text_bytes, $text ) {
- $this->assertNodeStart( "text", false );
- if ( $text_bytes !== false ) {
- $this->assertEquals( $this->xml->getAttribute( "bytes" ), $text_bytes,
- "Attribute 'bytes' of revision " . $id );
- }
-
- if ( $text === false ) {
- // Testing for a stub
- $this->assertEquals( $this->xml->getAttribute( "id" ), $text_id,
- "Text id of revision " . $id );
- $this->assertFalse( $this->xml->hasValue, "Revision has text" );
- $this->assertTrue( $this->xml->read(), "Skipping text start tag" );
- if ( ( $this->xml->nodeType == XMLReader::END_ELEMENT )
- && ( $this->xml->name == "text" )
- ) {
- $this->xml->read();
- }
- $this->skipWhitespace();
- } else {
- // Testing for a real dump
- $this->assertTrue( $this->xml->read(), "Skipping text start tag" );
- $this->assertEquals( $text, $this->xml->value, "Text of revision " . $id );
- $this->assertTrue( $this->xml->read(), "Skipping past text" );
- $this->assertNodeEnd( "text" );
- $this->skipWhitespace();
- }
- }
}
$dumper->dump( WikiExporter::FULL, WikiExporter::TEXT );
// Checking for correctness of the dumped data
- $this->assertDumpStart( $nameFull );
+ $asserter = $this->getDumpAsserter();
+ $asserter->assertDumpStart( $nameFull );
// Page 1
- $this->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" );
- $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
+ $asserter->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" );
+ $asserter->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
$this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87",
"BackupDumperTestP1Text1" );
- $this->assertPageEnd();
+ $asserter->assertPageEnd();
// Page 2
- $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" );
- $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1",
+ $asserter->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" );
+ $asserter->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1",
$this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2",
"BackupDumperTestP2Text1" );
- $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2",
+ $asserter->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2",
$this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95",
"BackupDumperTestP2Text2", $this->revId2_1 );
- $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3",
+ $asserter->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3",
$this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r",
"BackupDumperTestP2Text3", $this->revId2_2 );
- $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
+ $asserter->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
$this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv",
"BackupDumperTestP2Text4 some additional Text", $this->revId2_3 );
- $this->assertPageEnd();
+ $asserter->assertPageEnd();
// Page 3
// -> Page is marked deleted. Hence not visible
// Page 4
- $this->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" );
- $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
+ $asserter->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" );
+ $asserter->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
$this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe",
"TALK ABOUT BACKUPDUMPERTESTP1 TEXT1",
false,
"BackupTextPassTestModel",
"text/plain" );
- $this->assertPageEnd();
+ $asserter->assertPageEnd();
- $this->assertDumpEnd();
+ $asserter->assertDumpEnd();
}
function testPrefetchPlain() {
$dumper->dump( WikiExporter::FULL, WikiExporter::TEXT );
// Checking for correctness of the dumped data
- $this->assertDumpStart( $nameFull );
+ $asserter = $this->getDumpAsserter();
+ $asserter->assertDumpStart( $nameFull );
// Page 1
- $this->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" );
+ $asserter->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" );
// Prefetch kicks in. This is still the SHA-1 of the original text,
// But the actual text (with different SHA-1) comes from prefetch.
- $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
+ $asserter->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
$this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87",
"Prefetch_________1Text1" );
- $this->assertPageEnd();
+ $asserter->assertPageEnd();
// Page 2
- $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" );
- $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1",
+ $asserter->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" );
+ $asserter->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1",
$this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2",
"BackupDumperTestP2Text1" );
- $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2",
+ $asserter->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2",
$this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95",
"BackupDumperTestP2Text2", $this->revId2_1 );
// Prefetch kicks in. This is still the SHA-1 of the original text,
// But the actual text (with different SHA-1) comes from prefetch.
- $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3",
+ $asserter->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3",
$this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r",
"Prefetch_________2Text3", $this->revId2_2 );
- $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
+ $asserter->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
$this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv",
"BackupDumperTestP2Text4 some additional Text", $this->revId2_3 );
- $this->assertPageEnd();
+ $asserter->assertPageEnd();
// Page 3
// -> Page is marked deleted. Hence not visible
// Page 4
- $this->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" );
- $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
+ $asserter->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" );
+ $asserter->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
$this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe",
"TALK ABOUT BACKUPDUMPERTESTP1 TEXT1",
false,
"BackupTextPassTestModel",
"text/plain" );
- $this->assertPageEnd();
+ $asserter->assertPageEnd();
- $this->assertDumpEnd();
+ $asserter->assertDumpEnd();
}
/**
$lookingForPage = 1;
$checkpointFiles = 0;
+ $asserter = $this->getDumpAsserter();
+
// Each run of the following loop body tries to handle exactly 1 /page/ (not
// iteration of stub content). $i is only increased after having treated page 4.
for ( $i = 0; $i < $iterations; ) {
if ( $checkpointFormat == "gzip" ) {
$this->gunzip( $nameOutputDir . "/" . $fname );
}
- $this->assertDumpStart( $nameOutputDir . "/" . $fname );
+ $asserter->assertDumpStart( $nameOutputDir . "/" . $fname );
$fileOpened = true;
$checkpointFiles++;
}
switch ( $lookingForPage ) {
case 1:
// Page 1
- $this->assertPageStart( $this->pageId1 + $i * self::$numOfPages, NS_MAIN,
- "BackupDumperTestP1" );
- $this->assertRevision( $this->revId1_1 + $i * self::$numOfRevs, "BackupDumperTestP1Summary1",
- $this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87",
- "BackupDumperTestP1Text1" );
- $this->assertPageEnd();
+ $asserter->assertPageStart(
+ $this->pageId1 + $i * self::$numOfPages,
+ NS_MAIN,
+ "BackupDumperTestP1"
+ );
+ $asserter->assertRevision(
+ $this->revId1_1 + $i * self::$numOfRevs,
+ "BackupDumperTestP1Summary1",
+ $this->textId1_1,
+ false,
+ "0bolhl6ol7i6x0e7yq91gxgaan39j87",
+ "BackupDumperTestP1Text1"
+ );
+ $asserter->assertPageEnd();
$lookingForPage = 2;
break;
case 2:
// Page 2
- $this->assertPageStart( $this->pageId2 + $i * self::$numOfPages, NS_MAIN,
- "BackupDumperTestP2" );
- $this->assertRevision( $this->revId2_1 + $i * self::$numOfRevs, "BackupDumperTestP2Summary1",
- $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2",
- "BackupDumperTestP2Text1" );
- $this->assertRevision( $this->revId2_2 + $i * self::$numOfRevs, "BackupDumperTestP2Summary2",
- $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95",
- "BackupDumperTestP2Text2", $this->revId2_1 + $i * self::$numOfRevs );
- $this->assertRevision( $this->revId2_3 + $i * self::$numOfRevs, "BackupDumperTestP2Summary3",
- $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r",
- "BackupDumperTestP2Text3", $this->revId2_2 + $i * self::$numOfRevs );
- $this->assertRevision( $this->revId2_4 + $i * self::$numOfRevs,
+ $asserter->assertPageStart(
+ $this->pageId2 + $i * self::$numOfPages,
+ NS_MAIN,
+ "BackupDumperTestP2"
+ );
+ $asserter->assertRevision(
+ $this->revId2_1 + $i * self::$numOfRevs,
+ "BackupDumperTestP2Summary1",
+ $this->textId2_1,
+ false,
+ "jprywrymfhysqllua29tj3sc7z39dl2",
+ "BackupDumperTestP2Text1"
+ );
+ $asserter->assertRevision(
+ $this->revId2_2 + $i * self::$numOfRevs,
+ "BackupDumperTestP2Summary2",
+ $this->textId2_2,
+ false,
+ "b7vj5ks32po5m1z1t1br4o7scdwwy95",
+ "BackupDumperTestP2Text2",
+ $this->revId2_1 + $i * self::$numOfRevs
+ );
+ $asserter->assertRevision(
+ $this->revId2_3 + $i * self::$numOfRevs,
+ "BackupDumperTestP2Summary3",
+ $this->textId2_3,
+ false,
+ "jfunqmh1ssfb8rs43r19w98k28gg56r",
+ "BackupDumperTestP2Text3",
+ $this->revId2_2 + $i * self::$numOfRevs
+ );
+ $asserter->assertRevision(
+ $this->revId2_4 + $i * self::$numOfRevs,
"BackupDumperTestP2Summary4 extra",
- $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv",
+ $this->textId2_4,
+ false,
+ "6o1ciaxa6pybnqprmungwofc4lv00wv",
"BackupDumperTestP2Text4 some additional Text",
- $this->revId2_3 + $i * self::$numOfRevs );
- $this->assertPageEnd();
+ $this->revId2_3 + $i * self::$numOfRevs
+ );
+ $asserter->assertPageEnd();
$lookingForPage = 4;
break;
case 4:
// Page 4
- $this->assertPageStart( $this->pageId4 + $i * self::$numOfPages, NS_TALK,
- "Talk:BackupDumperTestP1" );
- $this->assertRevision( $this->revId4_1 + $i * self::$numOfRevs,
+ $asserter->assertPageStart(
+ $this->pageId4 + $i * self::$numOfPages,
+ NS_TALK,
+ "Talk:BackupDumperTestP1"
+ );
+ $asserter->assertRevision(
+ $this->revId4_1 + $i * self::$numOfRevs,
"Talk BackupDumperTestP1 Summary1",
- $this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe",
+ $this->textId4_1,
+ false,
+ "nktofwzd0tl192k3zfepmlzxoax1lpe",
"TALK ABOUT BACKUPDUMPERTESTP1 TEXT1",
false,
"BackupTextPassTestModel",
- "text/plain" );
- $this->assertPageEnd();
+ "text/plain"
+ );
+ $asserter->assertPageEnd();
$lookingForPage = 1;
if ( $this->xml->nodeType == XMLReader::END_ELEMENT
&& $this->xml->name == "mediawiki"
) {
- $this->assertDumpEnd();
+ $asserter->assertDumpEnd();
$fileOpened = false;
}
}
namespace MediaWiki\Tests\Maintenance;
+use Exception;
use MediaWiki\MediaWikiServices;
use DumpBackup;
use ManualLogEntry;
}
}
- /**
- * asserts that the xml reader is at the beginning of a log entry and skips over
- * it while analyzing it.
- *
- * @param int $id Id of the log entry
- * @param string $user_name User name of the log entry's performer
- * @param int $user_id User id of the log entry 's performer
- * @param string|null $comment Comment of the log entry. If null, the comment text is ignored.
- * @param string $type Type of the log entry
- * @param string $subtype Subtype of the log entry
- * @param string $title Title of the log entry's target
- * @param array $parameters (optional) unserialized data accompanying the log entry
- */
- private function assertLogItem( $id, $user_name, $user_id, $comment, $type,
- $subtype, $title, $parameters = []
- ) {
- $this->assertNodeStart( "logitem" );
- $this->skipWhitespace();
-
- $this->assertTextNode( "id", $id );
- $this->assertTextNode( "timestamp", false );
-
- $this->assertNodeStart( "contributor" );
- $this->skipWhitespace();
- $this->assertTextNode( "username", $user_name );
- $this->assertTextNode( "id", $user_id );
- $this->assertNodeEnd( "contributor" );
- $this->skipWhitespace();
-
- if ( $comment !== null ) {
- $this->assertTextNode( "comment", $comment );
- }
- $this->assertTextNode( "type", $type );
- $this->assertTextNode( "action", $subtype );
- $this->assertTextNode( "logtitle", $title );
-
- $this->assertNodeStart( "params" );
- $parameters_xml = unserialize( $this->xml->value );
- $this->assertEquals( $parameters, $parameters_xml );
- $this->assertTrue( $this->xml->read(), "Skipping past processed text of params" );
- $this->assertNodeEnd( "params" );
- $this->skipWhitespace();
-
- $this->assertNodeEnd( "logitem" );
- $this->skipWhitespace();
- }
-
function testPlain() {
// Preparing the dump
$fname = $this->getNewTempFile();
$dumper->dump( WikiExporter::LOGS, WikiExporter::TEXT );
// Analyzing the dumped data
- $this->assertDumpStart( $fname );
+ $this->assertDumpSchema( $fname, $this->getXmlSchemaPath() );
+
+ $asserter = $this->getDumpAsserter();
+ $asserter->assertDumpStart( $fname );
- $this->assertLogItem( $this->logId1, "BackupDumperLogUserA",
+ $asserter->assertLogItem( $this->logId1, "BackupDumperLogUserA",
$this->userId1, null, "type", "subtype", "PageA" );
$contLang = MediaWikiServices::getInstance()->getContentLanguage();
$namespace = $contLang->getNsText( NS_TALK );
$this->assertInternalType( 'string', $namespace );
$this->assertGreaterThan( 0, strlen( $namespace ) );
- $this->assertLogItem( $this->logId2, "BackupDumperLogUserB",
+ $asserter->assertLogItem( $this->logId2, "BackupDumperLogUserB",
$this->userId2, "SomeComment", "supress", "delete",
$namespace . ":PageB" );
- $this->assertLogItem( $this->logId3, "BackupDumperLogUserB",
+ $asserter->assertLogItem( $this->logId3, "BackupDumperLogUserB",
$this->userId2, "SomeOtherComment", "move", "delete",
"PageA", [ 'key1' => 1, 3 => 'value3' ] );
- $this->assertDumpEnd();
+ $asserter->assertDumpEnd();
}
function testXmlDumpsBackupUseCaseLogging() {
// Analyzing the dumped data
$this->gunzip( $fname );
- $this->assertDumpStart( $fname );
+ $this->assertDumpSchema( $fname, $this->getXmlSchemaPath() );
+
+ $asserter = $this->getDumpAsserter();
+ $asserter->assertDumpStart( $fname );
- $this->assertLogItem( $this->logId1, "BackupDumperLogUserA",
+ $asserter->assertLogItem( $this->logId1, "BackupDumperLogUserA",
$this->userId1, null, "type", "subtype", "PageA" );
$contLang = MediaWikiServices::getInstance()->getContentLanguage();
$namespace = $contLang->getNsText( NS_TALK );
$this->assertInternalType( 'string', $namespace );
$this->assertGreaterThan( 0, strlen( $namespace ) );
- $this->assertLogItem( $this->logId2, "BackupDumperLogUserB",
+ $asserter->assertLogItem( $this->logId2, "BackupDumperLogUserB",
$this->userId2, "SomeComment", "supress", "delete",
$namespace . ":PageB" );
- $this->assertLogItem( $this->logId3, "BackupDumperLogUserB",
+ $asserter->assertLogItem( $this->logId3, "BackupDumperLogUserB",
$this->userId2, "SomeOtherComment", "move", "delete",
"PageA", [ 'key1' => 1, 3 => 'value3' ] );
- $this->assertDumpEnd();
+ $asserter->assertDumpEnd();
// Currently, no reporting is implemented. Alert via failure, once
// this changes.
namespace MediaWiki\Tests\Maintenance;
use DumpBackup;
+use Exception;
use MediaWiki\MediaWikiServices;
use MediaWikiTestCase;
use MWException;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\LoadBalancer;
use WikiPage;
+use XmlDumpWriter;
/**
* Tests for page dumps of BackupDumper
return $dumper;
}
- function testFullTextPlain() {
+ public function schemaVersionProvider() {
+ foreach ( XmlDumpWriter::$supportedSchemas as $schemaVersion ) {
+ yield [ $schemaVersion ];
+ }
+ }
+
+ /**
+ * @dataProvider schemaVersionProvider
+ */
+ function testFullTextPlain( $schemaVersion ) {
// Preparing the dump
$fname = $this->getNewTempFile();
$dumper = $this->newDumpBackup(
- [ '--full', '--quiet', '--output', 'file:' . $fname ],
+ [ '--full', '--quiet', '--output', 'file:' . $fname, '--schema-version', $schemaVersion ],
$this->pageId1,
$this->pageId4 + 1
);
$dumper->execute();
// Checking the dumped data
- $this->assertDumpStart( $fname );
+ $this->assertDumpSchema( $fname, $this->getXmlSchemaPath( $schemaVersion ) );
+ $asserter = $this->getDumpAsserter( $schemaVersion );
+
+ $asserter->assertDumpStart( $fname );
// Page 1
- $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() );
- $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
- $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87",
- "BackupDumperTestP1Text1" );
- $this->assertPageEnd();
+ $asserter->assertPageStart(
+ $this->pageId1,
+ $this->namespace,
+ $this->pageTitle1->getPrefixedText()
+ );
+ $asserter->assertRevision(
+ $this->revId1_1,
+ "BackupDumperTestP1Summary1",
+ $this->textId1_1,
+ 23,
+ "0bolhl6ol7i6x0e7yq91gxgaan39j87",
+ "BackupDumperTestP1Text1"
+ );
+ $asserter->assertPageEnd();
// Page 2
- $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() );
- $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1",
- $this->textId2_1, 23, "jprywrymfhysqllua29tj3sc7z39dl2",
- "BackupDumperTestP2Text1" );
- $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2",
- $this->textId2_2, 23, "b7vj5ks32po5m1z1t1br4o7scdwwy95",
- "BackupDumperTestP2Text2", $this->revId2_1 );
- $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3",
- $this->textId2_3, 23, "jfunqmh1ssfb8rs43r19w98k28gg56r",
- "BackupDumperTestP2Text3", $this->revId2_2 );
- $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
- $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv",
- "BackupDumperTestP2Text4 some additional Text", $this->revId2_3 );
- $this->assertPageEnd();
+ $asserter->assertPageStart(
+ $this->pageId2,
+ $this->namespace,
+ $this->pageTitle2->getPrefixedText()
+ );
+ $asserter->assertRevision(
+ $this->revId2_1,
+ "BackupDumperTestP2Summary1",
+ $this->textId2_1,
+ 23,
+ "jprywrymfhysqllua29tj3sc7z39dl2",
+ "BackupDumperTestP2Text1"
+ );
+ $asserter->assertRevision(
+ $this->revId2_2,
+ "BackupDumperTestP2Summary2",
+ $this->textId2_2,
+ 23,
+ "b7vj5ks32po5m1z1t1br4o7scdwwy95",
+ "BackupDumperTestP2Text2",
+ $this->revId2_1
+ );
+ $asserter->assertRevision(
+ $this->revId2_3,
+ "BackupDumperTestP2Summary3",
+ $this->textId2_3,
+ 23,
+ "jfunqmh1ssfb8rs43r19w98k28gg56r",
+ "BackupDumperTestP2Text3",
+ $this->revId2_2
+ );
+ $asserter->assertRevision(
+ $this->revId2_4,
+ "BackupDumperTestP2Summary4 extra",
+ $this->textId2_4,
+ 44,
+ "6o1ciaxa6pybnqprmungwofc4lv00wv",
+ "BackupDumperTestP2Text4 some additional Text",
+ $this->revId2_3
+ );
+ $asserter->assertPageEnd();
// Page 3
// -> Page is marked deleted. Hence not visible
// Page 4
- $this->assertPageStart(
+ $asserter->assertPageStart(
$this->pageId4,
$this->talk_namespace,
$this->pageTitle4->getPrefixedText()
);
- $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
- $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe",
- "Talk about BackupDumperTestP1 Text1" );
- $this->assertPageEnd();
+ $asserter->assertRevision(
+ $this->revId4_1,
+ "Talk BackupDumperTestP1 Summary1",
+ $this->textId4_1,
+ 35,
+ "nktofwzd0tl192k3zfepmlzxoax1lpe",
+ "Talk about BackupDumperTestP1 Text1",
+ false,
+ CONTENT_MODEL_WIKITEXT,
+ CONTENT_FORMAT_WIKITEXT,
+ $schemaVersion
+ );
+ $asserter->assertPageEnd();
- $this->assertDumpEnd();
+ $asserter->assertDumpEnd();
+
+ // FIXME: add multi-slot test case!
}
- function testFullStubPlain() {
+ /**
+ * @dataProvider schemaVersionProvider
+ */
+ function testFullStubPlain( $schemaVersion ) {
// Preparing the dump
$fname = $this->getNewTempFile();
$dumper = $this->newDumpBackup(
- [ '--full', '--quiet', '--output', 'file:' . $fname, '--stub' ],
+ [
+ '--full',
+ '--quiet',
+ '--output',
+ 'file:' . $fname,
+ '--stub',
+ '--schema-version', $schemaVersion,
+ ],
$this->pageId1,
$this->pageId4 + 1
);
$dumper->execute();
// Checking the dumped data
- $this->assertDumpStart( $fname );
+ $this->assertDumpSchema( $fname, $this->getXmlSchemaPath( $schemaVersion ) );
+ $asserter = $this->getDumpAsserter( $schemaVersion );
+
+ $asserter->assertDumpStart( $fname );
// Page 1
- $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() );
- $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
- $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" );
- $this->assertPageEnd();
+ $asserter->assertPageStart(
+ $this->pageId1,
+ $this->namespace,
+ $this->pageTitle1->getPrefixedText()
+ );
+ $asserter->assertRevision(
+ $this->revId1_1,
+ "BackupDumperTestP1Summary1",
+ $this->textId1_1,
+ 23,
+ "0bolhl6ol7i6x0e7yq91gxgaan39j87"
+ );
+ $asserter->assertPageEnd();
// Page 2
- $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() );
- $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1",
- $this->textId2_1, 23, "jprywrymfhysqllua29tj3sc7z39dl2" );
- $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2",
- $this->textId2_2, 23, "b7vj5ks32po5m1z1t1br4o7scdwwy95", false, $this->revId2_1 );
- $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3",
- $this->textId2_3, 23, "jfunqmh1ssfb8rs43r19w98k28gg56r", false, $this->revId2_2 );
- $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
- $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 );
- $this->assertPageEnd();
+ $asserter->assertPageStart(
+ $this->pageId2,
+ $this->namespace,
+ $this->pageTitle2->getPrefixedText()
+ );
+ $asserter->assertRevision(
+ $this->revId2_1,
+ "BackupDumperTestP2Summary1",
+ $this->textId2_1,
+ 23,
+ "jprywrymfhysqllua29tj3sc7z39dl2"
+ );
+ $asserter->assertRevision(
+ $this->revId2_2,
+ "BackupDumperTestP2Summary2",
+ $this->textId2_2,
+ 23,
+ "b7vj5ks32po5m1z1t1br4o7scdwwy95",
+ false,
+ $this->revId2_1
+ );
+ $asserter->assertRevision(
+ $this->revId2_3,
+ "BackupDumperTestP2Summary3",
+ $this->textId2_3,
+ 23,
+ "jfunqmh1ssfb8rs43r19w98k28gg56r",
+ false,
+ $this->revId2_2
+ );
+ $asserter->assertRevision(
+ $this->revId2_4,
+ "BackupDumperTestP2Summary4 extra",
+ $this->textId2_4,
+ 44,
+ "6o1ciaxa6pybnqprmungwofc4lv00wv",
+ false,
+ $this->revId2_3
+ );
+ $asserter->assertPageEnd();
// Page 3
// -> Page is marked deleted. Hence not visible
// Page 4
- $this->assertPageStart(
+ $asserter->assertPageStart(
$this->pageId4,
$this->talk_namespace,
$this->pageTitle4->getPrefixedText()
);
- $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
- $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" );
- $this->assertPageEnd();
+ $asserter->assertRevision(
+ $this->revId4_1,
+ "Talk BackupDumperTestP1 Summary1",
+ $this->textId4_1,
+ 35,
+ "nktofwzd0tl192k3zfepmlzxoax1lpe"
+ );
+ $asserter->assertPageEnd();
- $this->assertDumpEnd();
+ $asserter->assertDumpEnd();
}
- function testCurrentStubPlain() {
+ /**
+ * @dataProvider schemaVersionProvider
+ */
+ function testCurrentStubPlain( $schemaVersion ) {
// Preparing the dump
$fname = $this->getNewTempFile();
$dumper = $this->newDumpBackup(
- [ '--output', 'file:' . $fname ],
+ [ '--output', 'file:' . $fname, '--schema-version', $schemaVersion ],
$this->pageId1,
$this->pageId4 + 1
);
$dumper->dump( WikiExporter::CURRENT, WikiExporter::STUB );
// Checking the dumped data
- $this->assertDumpStart( $fname );
+ $this->assertDumpSchema( $fname, $this->getXmlSchemaPath( $schemaVersion ) );
+
+ $asserter = $this->getDumpAsserter( $schemaVersion );
+ $asserter->assertDumpStart( $fname );
// Page 1
- $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() );
- $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
- $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" );
- $this->assertPageEnd();
+ $asserter->assertPageStart(
+ $this->pageId1,
+ $this->namespace,
+ $this->pageTitle1->getPrefixedText()
+ );
+ $asserter->assertRevision(
+ $this->revId1_1,
+ "BackupDumperTestP1Summary1",
+ $this->textId1_1,
+ 23,
+ "0bolhl6ol7i6x0e7yq91gxgaan39j87"
+ );
+ $asserter->assertPageEnd();
// Page 2
- $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() );
- $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
- $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 );
- $this->assertPageEnd();
+ $asserter->assertPageStart(
+ $this->pageId2,
+ $this->namespace,
+ $this->pageTitle2->getPrefixedText()
+ );
+ $asserter->assertRevision(
+ $this->revId2_4,
+ "BackupDumperTestP2Summary4 extra",
+ $this->textId2_4,
+ 44,
+ "6o1ciaxa6pybnqprmungwofc4lv00wv",
+ false,
+ $this->revId2_3
+ );
+ $asserter->assertPageEnd();
// Page 3
// -> Page is marked deleted. Hence not visible
// Page 4
- $this->assertPageStart(
+ $asserter->assertPageStart(
$this->pageId4,
$this->talk_namespace,
$this->pageTitle4->getPrefixedText()
);
- $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
- $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" );
- $this->assertPageEnd();
+ $asserter->assertRevision(
+ $this->revId4_1,
+ "Talk BackupDumperTestP1 Summary1",
+ $this->textId4_1,
+ 35,
+ "nktofwzd0tl192k3zfepmlzxoax1lpe"
+ );
+ $asserter->assertPageEnd();
- $this->assertDumpEnd();
+ $asserter->assertDumpEnd();
}
function testCurrentStubGzip() {
// Checking the dumped data
$this->gunzip( $fname );
- $this->assertDumpStart( $fname );
+
+ $asserter = $this->getDumpAsserter();
+ $asserter->assertDumpStart( $fname );
// Page 1
- $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() );
- $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
- $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" );
- $this->assertPageEnd();
+ $asserter->assertPageStart(
+ $this->pageId1,
+ $this->namespace,
+ $this->pageTitle1->getPrefixedText()
+ );
+ $asserter->assertRevision(
+ $this->revId1_1,
+ "BackupDumperTestP1Summary1",
+ $this->textId1_1,
+ 23,
+ "0bolhl6ol7i6x0e7yq91gxgaan39j87"
+ );
+ $asserter->assertPageEnd();
// Page 2
- $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() );
- $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
- $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 );
- $this->assertPageEnd();
+ $asserter->assertPageStart(
+ $this->pageId2,
+ $this->namespace,
+ $this->pageTitle2->getPrefixedText()
+ );
+ $asserter->assertRevision(
+ $this->revId2_4,
+ "BackupDumperTestP2Summary4 extra",
+ $this->textId2_4,
+ 44,
+ "6o1ciaxa6pybnqprmungwofc4lv00wv",
+ false,
+ $this->revId2_3
+ );
+ $asserter->assertPageEnd();
// Page 3
// -> Page is marked deleted. Hence not visible
// Page 4
- $this->assertPageStart(
+ $asserter->assertPageStart(
$this->pageId4,
$this->talk_namespace,
$this->pageTitle4->getPrefixedText()
);
- $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
+ $asserter->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
$this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" );
- $this->assertPageEnd();
+ $asserter->assertPageEnd();
- $this->assertDumpEnd();
+ $asserter->assertDumpEnd();
}
/**
*
* We reproduce such a setup with our mini fixture, although we omit
* chunks, and all the other gimmicks of xmldumps-backup.
+ *
+ * @dataProvider schemaVersionProvider
*/
- function testXmlDumpsBackupUseCase() {
+ function testXmlDumpsBackupUseCase( $schemaVersion ) {
$this->checkHasGzip();
$fnameMetaHistory = $this->getNewTempFile();
"--output=gzip:" . $fnameMetaCurrent, "--filter=latest",
"--output=gzip:" . $fnameArticles, "--filter=latest",
"--filter=notalk", "--filter=namespace:!NS_USER",
- "--reporting=1000"
+ "--reporting=1000", '--schema-version', $schemaVersion
],
$this->pageId1,
$this->pageId4 + 1
// Checking meta-history -------------------------------------------------
$this->gunzip( $fnameMetaHistory );
- $this->assertDumpStart( $fnameMetaHistory );
+ $this->assertDumpSchema( $fnameMetaHistory, $this->getXmlSchemaPath( $schemaVersion ) );
+
+ $asserter = $this->getDumpAsserter( $schemaVersion );
+ $asserter->assertDumpStart( $fnameMetaHistory );
// Page 1
- $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() );
- $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
- $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" );
- $this->assertPageEnd();
+ $asserter->assertPageStart(
+ $this->pageId1,
+ $this->namespace,
+ $this->pageTitle1->getPrefixedText()
+ );
+ $asserter->assertRevision(
+ $this->revId1_1,
+ "BackupDumperTestP1Summary1",
+ $this->textId1_1,
+ 23,
+ "0bolhl6ol7i6x0e7yq91gxgaan39j87"
+ );
+ $asserter->assertPageEnd();
// Page 2
- $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() );
- $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1",
- $this->textId2_1, 23, "jprywrymfhysqllua29tj3sc7z39dl2" );
- $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2",
- $this->textId2_2, 23, "b7vj5ks32po5m1z1t1br4o7scdwwy95", false, $this->revId2_1 );
- $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3",
- $this->textId2_3, 23, "jfunqmh1ssfb8rs43r19w98k28gg56r", false, $this->revId2_2 );
- $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
- $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 );
- $this->assertPageEnd();
+ $asserter->assertPageStart(
+ $this->pageId2,
+ $this->namespace,
+ $this->pageTitle2->getPrefixedText()
+ );
+ $asserter->assertRevision(
+ $this->revId2_1,
+ "BackupDumperTestP2Summary1",
+ $this->textId2_1,
+ 23,
+ "jprywrymfhysqllua29tj3sc7z39dl2"
+ );
+ $asserter->assertRevision(
+ $this->revId2_2,
+ "BackupDumperTestP2Summary2",
+ $this->textId2_2,
+ 23,
+ "b7vj5ks32po5m1z1t1br4o7scdwwy95",
+ false,
+ $this->revId2_1
+ );
+ $asserter->assertRevision(
+ $this->revId2_3,
+ "BackupDumperTestP2Summary3",
+ $this->textId2_3,
+ 23,
+ "jfunqmh1ssfb8rs43r19w98k28gg56r",
+ false,
+ $this->revId2_2
+ );
+ $asserter->assertRevision(
+ $this->revId2_4,
+ "BackupDumperTestP2Summary4 extra",
+ $this->textId2_4,
+ 44,
+ "6o1ciaxa6pybnqprmungwofc4lv00wv",
+ false,
+ $this->revId2_3
+ );
+ $asserter->assertPageEnd();
// Page 3
// -> Page is marked deleted. Hence not visible
// Page 4
- $this->assertPageStart(
+ $asserter->assertPageStart(
$this->pageId4,
$this->talk_namespace,
- $this->pageTitle4->getPrefixedText()
+ $this->pageTitle4->getPrefixedText( $schemaVersion )
);
- $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
- $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" );
- $this->assertPageEnd();
+ $asserter->assertRevision(
+ $this->revId4_1,
+ "Talk BackupDumperTestP1 Summary1",
+ $this->textId4_1,
+ 35,
+ "nktofwzd0tl192k3zfepmlzxoax1lpe"
+ );
+ $asserter->assertPageEnd();
- $this->assertDumpEnd();
+ $asserter->assertDumpEnd();
// Checking meta-current -------------------------------------------------
$this->gunzip( $fnameMetaCurrent );
- $this->assertDumpStart( $fnameMetaCurrent );
+ $this->assertDumpSchema( $fnameMetaCurrent, $this->getXmlSchemaPath( $schemaVersion ) );
+
+ $asserter = $this->getDumpAsserter( $schemaVersion );
+ $asserter->assertDumpStart( $fnameMetaCurrent );
// Page 1
- $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() );
- $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
- $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" );
- $this->assertPageEnd();
+ $asserter->assertPageStart(
+ $this->pageId1,
+ $this->namespace,
+ $this->pageTitle1->getPrefixedText()
+ );
+ $asserter->assertRevision(
+ $this->revId1_1,
+ "BackupDumperTestP1Summary1",
+ $this->textId1_1,
+ 23,
+ "0bolhl6ol7i6x0e7yq91gxgaan39j87"
+ );
+ $asserter->assertPageEnd();
// Page 2
- $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() );
- $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
- $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 );
- $this->assertPageEnd();
+ $asserter->assertPageStart(
+ $this->pageId2,
+ $this->namespace,
+ $this->pageTitle2->getPrefixedText()
+ );
+ $asserter->assertRevision(
+ $this->revId2_4,
+ "BackupDumperTestP2Summary4 extra",
+ $this->textId2_4,
+ 44,
+ "6o1ciaxa6pybnqprmungwofc4lv00wv",
+ false,
+ $this->revId2_3
+ );
+ $asserter->assertPageEnd();
// Page 3
// -> Page is marked deleted. Hence not visible
// Page 4
- $this->assertPageStart(
+ $asserter->assertPageStart(
$this->pageId4,
$this->talk_namespace,
$this->pageTitle4->getPrefixedText()
);
- $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
- $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" );
- $this->assertPageEnd();
+ $asserter->assertRevision(
+ $this->revId4_1,
+ "Talk BackupDumperTestP1 Summary1",
+ $this->textId4_1,
+ 35,
+ "nktofwzd0tl192k3zfepmlzxoax1lpe"
+ );
+ $asserter->assertPageEnd();
- $this->assertDumpEnd();
+ $asserter->assertDumpEnd();
// Checking articles -------------------------------------------------
$this->gunzip( $fnameArticles );
- $this->assertDumpStart( $fnameArticles );
+ $this->assertDumpSchema( $fnameArticles, $this->getXmlSchemaPath( $schemaVersion ) );
+
+ $asserter = $this->getDumpAsserter( $schemaVersion );
+ $asserter->assertDumpStart( $fnameArticles );
// Page 1
- $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() );
- $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
- $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" );
- $this->assertPageEnd();
+ $asserter->assertPageStart(
+ $this->pageId1,
+ $this->namespace,
+ $this->pageTitle1->getPrefixedText()
+ );
+ $asserter->assertRevision(
+ $this->revId1_1,
+ "BackupDumperTestP1Summary1",
+ $this->textId1_1,
+ 23,
+ "0bolhl6ol7i6x0e7yq91gxgaan39j87"
+ );
+ $asserter->assertPageEnd();
// Page 2
- $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() );
- $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
- $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 );
- $this->assertPageEnd();
+ $asserter->assertPageStart(
+ $this->pageId2,
+ $this->namespace,
+ $this->pageTitle2->getPrefixedText()
+ );
+ $asserter->assertRevision(
+ $this->revId2_4,
+ "BackupDumperTestP2Summary4 extra",
+ $this->textId2_4,
+ 44,
+ "6o1ciaxa6pybnqprmungwofc4lv00wv",
+ false,
+ $this->revId2_3
+ );
+ $asserter->assertPageEnd();
// Page 3
// -> Page is marked deleted. Hence not visible
// Page 4
// -> Page is not in $this->namespace. Hence not visible
- $this->assertDumpEnd();
+ $asserter->assertDumpEnd();
$this->expectETAOutput();
}
--- /dev/null
+<?xml version='1.0'?>
+<?xml-stylesheet href="../2008/09/xsd.xsl" type="text/xsl"?>
+<xs:schema targetNamespace="http://www.w3.org/XML/1998/namespace"
+ xmlns:xs="http://www.w3.org/2001/XMLSchema"
+ xmlns ="http://www.w3.org/1999/xhtml"
+ xml:lang="en">
+
+ <xs:annotation>
+ <xs:documentation>
+ <div>
+ <h1>About the XML namespace</h1>
+
+ <div class="bodytext">
+ <p>
+ This schema document describes the XML namespace, in a form
+ suitable for import by other schema documents.
+ </p>
+ <p>
+ See <a href="http://www.w3.org/XML/1998/namespace.html">
+ http://www.w3.org/XML/1998/namespace.html</a> and
+ <a href="http://www.w3.org/TR/REC-xml">
+ http://www.w3.org/TR/REC-xml</a> for information
+ about this namespace.
+ </p>
+ <p>
+ Note that local names in this namespace are intended to be
+ defined only by the World Wide Web Consortium or its subgroups.
+ The names currently defined in this namespace are listed below.
+ They should not be used with conflicting semantics by any Working
+ Group, specification, or document instance.
+ </p>
+ <p>
+ See further below in this document for more information about <a
+ href="#usage">how to refer to this schema document from your own
+ XSD schema documents</a> and about <a href="#nsversioning">the
+ namespace-versioning policy governing this schema document</a>.
+ </p>
+ </div>
+ </div>
+ </xs:documentation>
+ </xs:annotation>
+
+ <xs:attribute name="lang">
+ <xs:annotation>
+ <xs:documentation>
+ <div>
+
+ <h3>lang (as an attribute name)</h3>
+ <p>
+ denotes an attribute whose value
+ is a language code for the natural language of the content of
+ any element; its value is inherited. This name is reserved
+ by virtue of its definition in the XML specification.</p>
+
+ </div>
+ <div>
+ <h4>Notes</h4>
+ <p>
+ Attempting to install the relevant ISO 2- and 3-letter
+ codes as the enumerated possible values is probably never
+ going to be a realistic possibility.
+ </p>
+ <p>
+ See BCP 47 at <a href="http://www.rfc-editor.org/rfc/bcp/bcp47.txt">
+ http://www.rfc-editor.org/rfc/bcp/bcp47.txt</a>
+ and the IANA language subtag registry at
+ <a href="http://www.iana.org/assignments/language-subtag-registry">
+ http://www.iana.org/assignments/language-subtag-registry</a>
+ for further information.
+ </p>
+ <p>
+ The union allows for the 'un-declaration' of xml:lang with
+ the empty string.
+ </p>
+ </div>
+ </xs:documentation>
+ </xs:annotation>
+ <xs:simpleType>
+ <xs:union memberTypes="xs:language">
+ <xs:simpleType>
+ <xs:restriction base="xs:string">
+ <xs:enumeration value=""/>
+ </xs:restriction>
+ </xs:simpleType>
+ </xs:union>
+ </xs:simpleType>
+ </xs:attribute>
+
+ <xs:attribute name="space">
+ <xs:annotation>
+ <xs:documentation>
+ <div>
+
+ <h3>space (as an attribute name)</h3>
+ <p>
+ denotes an attribute whose
+ value is a keyword indicating what whitespace processing
+ discipline is intended for the content of the element; its
+ value is inherited. This name is reserved by virtue of its
+ definition in the XML specification.</p>
+
+ </div>
+ </xs:documentation>
+ </xs:annotation>
+ <xs:simpleType>
+ <xs:restriction base="xs:NCName">
+ <xs:enumeration value="default"/>
+ <xs:enumeration value="preserve"/>
+ </xs:restriction>
+ </xs:simpleType>
+ </xs:attribute>
+
+ <xs:attribute name="base" type="xs:anyURI"> <xs:annotation>
+ <xs:documentation>
+ <div>
+
+ <h3>base (as an attribute name)</h3>
+ <p>
+ denotes an attribute whose value
+ provides a URI to be used as the base for interpreting any
+ relative URIs in the scope of the element on which it
+ appears; its value is inherited. This name is reserved
+ by virtue of its definition in the XML Base specification.</p>
+
+ <p>
+ See <a
+ href="http://www.w3.org/TR/xmlbase/">http://www.w3.org/TR/xmlbase/</a>
+ for information about this attribute.
+ </p>
+ </div>
+ </xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+
+ <xs:attribute name="id" type="xs:ID">
+ <xs:annotation>
+ <xs:documentation>
+ <div>
+
+ <h3>id (as an attribute name)</h3>
+ <p>
+ denotes an attribute whose value
+ should be interpreted as if declared to be of type ID.
+ This name is reserved by virtue of its definition in the
+ xml:id specification.</p>
+
+ <p>
+ See <a
+ href="http://www.w3.org/TR/xml-id/">http://www.w3.org/TR/xml-id/</a>
+ for information about this attribute.
+ </p>
+ </div>
+ </xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+
+ <xs:attributeGroup name="specialAttrs">
+ <xs:attribute ref="xml:base"/>
+ <xs:attribute ref="xml:lang"/>
+ <xs:attribute ref="xml:space"/>
+ <xs:attribute ref="xml:id"/>
+ </xs:attributeGroup>
+
+ <xs:annotation>
+ <xs:documentation>
+ <div>
+
+ <h3>Father (in any context at all)</h3>
+
+ <div class="bodytext">
+ <p>
+ denotes Jon Bosak, the chair of
+ the original XML Working Group. This name is reserved by
+ the following decision of the W3C XML Plenary and
+ XML Coordination groups:
+ </p>
+ <blockquote>
+ <p>
+ In appreciation for his vision, leadership and
+ dedication the W3C XML Plenary on this 10th day of
+ February, 2000, reserves for Jon Bosak in perpetuity
+ the XML name "xml:Father".
+ </p>
+ </blockquote>
+ </div>
+ </div>
+ </xs:documentation>
+ </xs:annotation>
+
+ <xs:annotation>
+ <xs:documentation>
+ <div xml:id="usage" id="usage">
+ <h2><a name="usage">About this schema document</a></h2>
+
+ <div class="bodytext">
+ <p>
+ This schema defines attributes and an attribute group suitable
+ for use by schemas wishing to allow <code>xml:base</code>,
+ <code>xml:lang</code>, <code>xml:space</code> or
+ <code>xml:id</code> attributes on elements they define.
+ </p>
+ <p>
+ To enable this, such a schema must import this schema for
+ the XML namespace, e.g. as follows:
+ </p>
+ <pre>
+ <schema . . .>
+ . . .
+ <import namespace="http://www.w3.org/XML/1998/namespace"
+ schemaLocation="http://www.w3.org/2001/xml.xsd"/>
+ </pre>
+ <p>
+ or
+ </p>
+ <pre>
+ <import namespace="http://www.w3.org/XML/1998/namespace"
+ schemaLocation="http://www.w3.org/2009/01/xml.xsd"/>
+ </pre>
+ <p>
+ Subsequently, qualified reference to any of the attributes or the
+ group defined below will have the desired effect, e.g.
+ </p>
+ <pre>
+ <type . . .>
+ . . .
+ <attributeGroup ref="xml:specialAttrs"/>
+ </pre>
+ <p>
+ will define a type which will schema-validate an instance element
+ with any of those attributes.
+ </p>
+ </div>
+ </div>
+ </xs:documentation>
+ </xs:annotation>
+
+ <xs:annotation>
+ <xs:documentation>
+ <div id="nsversioning" xml:id="nsversioning">
+ <h2><a name="nsversioning">Versioning policy for this schema document</a></h2>
+ <div class="bodytext">
+ <p>
+ In keeping with the XML Schema WG's standard versioning
+ policy, this schema document will persist at
+ <a href="http://www.w3.org/2009/01/xml.xsd">
+ http://www.w3.org/2009/01/xml.xsd</a>.
+ </p>
+ <p>
+ At the date of issue it can also be found at
+ <a href="http://www.w3.org/2001/xml.xsd">
+ http://www.w3.org/2001/xml.xsd</a>.
+ </p>
+ <p>
+ The schema document at that URI may however change in the future,
+ in order to remain compatible with the latest version of XML
+ Schema itself, or with the XML namespace itself. In other words,
+ if the XML Schema or XML namespaces change, the version of this
+ document at <a href="http://www.w3.org/2001/xml.xsd">
+ http://www.w3.org/2001/xml.xsd
+ </a>
+ will change accordingly; the version at
+ <a href="http://www.w3.org/2009/01/xml.xsd">
+ http://www.w3.org/2009/01/xml.xsd
+ </a>
+ will not change.
+ </p>
+ <p>
+ Previous dated (and unchanging) versions of this schema
+ document are at:
+ </p>
+ <ul>
+ <li><a href="http://www.w3.org/2009/01/xml.xsd">
+ http://www.w3.org/2009/01/xml.xsd</a></li>
+ <li><a href="http://www.w3.org/2007/08/xml.xsd">
+ http://www.w3.org/2007/08/xml.xsd</a></li>
+ <li><a href="http://www.w3.org/2004/10/xml.xsd">
+ http://www.w3.org/2004/10/xml.xsd</a></li>
+ <li><a href="http://www.w3.org/2001/03/xml.xsd">
+ http://www.w3.org/2001/03/xml.xsd</a></li>
+ </ul>
+ </div>
+ </div>
+ </xs:documentation>
+ </xs:annotation>
+
+</xs:schema>
+
"mocha": true
},
"globals": {
- "browser": false
+ "browser": false,
+ "mw": false
},
"rules": {
"no-console": 0
-const Page = require( 'wdio-mediawiki/Page' );
+const Page = require( 'wdio-mediawiki/Page' ),
+ Api = require( 'wdio-mediawiki/Api' );
class HistoryPage extends Page {
+ get heading() { return browser.element( '#firstHeading' ); }
+ get headingText() { return browser.getText( '#firstHeading' ); }
get comment() { return browser.element( '#pagehistory .comment' ); }
+ get rollback() { return browser.element( '.mw-rollback-link' ); }
+ get rollbackLink() { return browser.element( '.mw-rollback-link a' ); }
+ get rollbackConfirmable() { return browser.element( '.mw-rollback-link .jquery-confirmable-text' ); }
+ get rollbackConfirmableYes() { return browser.element( '.mw-rollback-link .jquery-confirmable-button-yes' ); }
+ get rollbackConfirmableNo() { return browser.element( '.mw-rollback-link .jquery-confirmable-button-no' ); }
+ get rollbackNonJsConfirmable() { return browser.element( '.mw-htmlform .oo-ui-fieldsetLayout-header .oo-ui-labelElement-label' ); }
+ get rollbackNonJsConfirmableYes() { return browser.element( '.mw-htmlform .mw-htmlform-submit-buttons button' ); }
open( title ) {
super.openTitle( title, { action: 'history' } );
}
+
+ vandalizePage( name, content ) {
+ let vandalUsername = 'Evil_' + browser.options.username;
+
+ browser.call( function () {
+ return Api.edit( name, content );
+ } );
+
+ browser.call( function () {
+ return Api.createAccount(
+ vandalUsername, browser.options.password
+ );
+ } );
+
+ browser.call( function () {
+ Api.edit(
+ name,
+ 'Vandalized: ' + content,
+ vandalUsername
+ );
+ } );
+ }
}
module.exports = new HistoryPage();
EditPage = require( '../pageobjects/edit.page' ),
HistoryPage = require( '../pageobjects/history.page' ),
UndoPage = require( '../pageobjects/undo.page' ),
- UserLoginPage = require( '../pageobjects/userlogin.page' ),
+ UserLoginPage = require( 'wdio-mediawiki/LoginPage' ),
Util = require( 'wdio-mediawiki/Util' );
describe( 'Page', function () {
// check
HistoryPage.open( name );
- assert.strictEqual( HistoryPage.comment.getText(), `(Created page with "${content}")` );
+ assert.strictEqual( HistoryPage.comment.getText(), `(Created or updated page with "${content}")` );
} );
it( 'should be deletable', function () {
--- /dev/null
+const assert = require( 'assert' ),
+ HistoryPage = require( '../pageobjects/history.page' ),
+ UserLoginPage = require( 'wdio-mediawiki/LoginPage' ),
+ Util = require( 'wdio-mediawiki/Util' );
+
+describe( 'Rollback with confirmation', function () {
+ var content,
+ name;
+
+ before( function () {
+ // disable VisualEditor welcome dialog
+ browser.deleteCookie();
+ UserLoginPage.open();
+ browser.localStorage( 'POST', { key: 've-beta-welcome-dialog', value: '1' } );
+
+ // Enable rollback confirmation for admin user
+ // Requires user to log in again, handled by deleteCookie() call in beforeEach function
+ UserLoginPage.loginAdmin();
+
+ browser.pause( 300 );
+ browser.execute( function () {
+ return ( new mw.Api() ).saveOption(
+ 'showrollbackconfirmation',
+ '1'
+ );
+ } );
+ } );
+
+ beforeEach( function () {
+ browser.deleteCookie();
+
+ content = Util.getTestString( 'beforeEach-content-' );
+ name = Util.getTestString( 'BeforeEach-name-' );
+
+ HistoryPage.vandalizePage( name, content );
+
+ UserLoginPage.loginAdmin();
+ HistoryPage.open( name );
+ } );
+
+ it( 'should offer rollback options for admin users', function () {
+ assert.strictEqual( HistoryPage.rollback.getText(), 'rollback 1 edit' );
+
+ HistoryPage.rollback.click();
+
+ assert.strictEqual( HistoryPage.rollbackConfirmable.getText(), 'Rollback of one edit?' );
+ assert.strictEqual( HistoryPage.rollbackConfirmableYes.getText(), 'Rollback' );
+ assert.strictEqual( HistoryPage.rollbackConfirmableNo.getText(), 'Cancel' );
+ } );
+
+ it( 'should offer a way to cancel rollbacks', function () {
+ HistoryPage.rollback.click();
+ HistoryPage.rollbackConfirmableNo.click();
+
+ browser.pause( 500 );
+
+ assert.strictEqual( HistoryPage.heading.getText(), 'Revision history of "' + name + '"' );
+ } );
+
+ it( 'should perform rollbacks after confirming intention', function () {
+ HistoryPage.rollback.click();
+ HistoryPage.rollbackConfirmableYes.click();
+
+ // waitUntil indirectly asserts that the content we are looking for is present
+ browser.waitUntil( function () {
+ return browser.getText( '#firstHeading' ) === 'Action complete';
+ }, 5000, 'Expected rollback page to appear.' );
+ } );
+
+ it( 'should verify rollbacks via GET requests are confirmed on a follow-up page', function () {
+ var rollbackActionUrl = HistoryPage.rollbackLink.getAttribute( 'href' );
+ browser.url( rollbackActionUrl );
+
+ browser.waitUntil( function () {
+ return HistoryPage.rollbackNonJsConfirmable.getText() === 'Revert edits to this page?';
+ }, 5000, 'Expected rollback confirmation page to appear for GET-based rollbacks.' );
+
+ HistoryPage.rollbackNonJsConfirmableYes.click();
+
+ browser.waitUntil( function () {
+ return browser.getText( '#firstHeading' ) === 'Action complete';
+ }, 5000, 'Expected rollback page to appear.' );
+ } );
+
+} );
+
+describe( 'Rollback without confirmation', function () {
+ var content,
+ name;
+
+ before( function () {
+ // disable VisualEditor welcome dialog
+ browser.deleteCookie();
+ UserLoginPage.open();
+ browser.localStorage( 'POST', { key: 've-beta-welcome-dialog', value: '1' } );
+
+ // Disable rollback confirmation for admin user
+ // Requires user to log in again, handled by deleteCookie() call in beforeEach function
+ UserLoginPage.loginAdmin();
+
+ browser.pause( 300 );
+ browser.execute( function () {
+ return ( new mw.Api() ).saveOption(
+ 'showrollbackconfirmation',
+ '0'
+ );
+ } );
+ } );
+
+ beforeEach( function () {
+ browser.deleteCookie();
+
+ content = Util.getTestString( 'beforeEach-content-' );
+ name = Util.getTestString( 'BeforeEach-name-' );
+
+ HistoryPage.vandalizePage( name, content );
+
+ UserLoginPage.loginAdmin();
+ HistoryPage.open( name );
+ } );
+
+ it( 'should perform rollback via POST request without asking the user to confirm', function () {
+ HistoryPage.rollback.click();
+
+ // waitUntil indirectly asserts that the content we are looking for is present
+ browser.waitUntil( function () {
+ return HistoryPage.headingText === 'Action complete';
+ }, 5000, 'Expected rollback page to appear.' );
+ } );
+
+ it( 'should perform rollback via GET request without asking the user to confirm', function () {
+ var rollbackActionUrl = HistoryPage.rollbackLink.getAttribute( 'href' );
+ browser.url( rollbackActionUrl );
+
+ browser.waitUntil( function () {
+ return browser.getText( '#firstHeading' ) === 'Action complete';
+ }, 5000, 'Expected rollback page to appear.' );
+ } );
+} );
module.exports = {
/**
* Shortcut for `MWBot#edit( .. )`.
+ * Default username, password and base URL is used unless specified
*
* @since 1.0.0
* @see <https://www.mediawiki.org/wiki/API:Edit>
* @param {string} title
* @param {string} content
+ * @param {string} username - Optional
+ * @param {string} password - Optional
+ * @param {baseUrl} baseUrl - Optional
* @return {Object} Promise for API action=edit response data.
*/
- edit( title, content ) {
+ edit( title,
+ content,
+ username = browser.options.username,
+ password = browser.options.password,
+ baseUrl = browser.options.baseUrl
+ ) {
let bot = new MWBot();
return bot.loginGetEditToken( {
- apiUrl: `${browser.options.baseUrl}/api.php`,
- username: browser.options.username,
- password: browser.options.password
+ apiUrl: `${baseUrl}/api.php`,
+ username: username,
+ password: password
} ).then( function () {
- return bot.edit( title, content, `Created page with "${content}"` );
+ return bot.edit( title, content, `Created or updated page with "${content}"` );
} );
},