and Special:AllMyUploads respectively.
* IPv6 addresses in X-Forwarded-For headers are now normalised before checking
against allowed proxy lists.
-* Add deferrable update support for callback/closure
-* Add TitleMove hook before page renames
+* Add deferrable update support for callback/closure.
+* Add TitleMove hook before page renames.
* Revision deletion backend code is moved out of SpecialRevisiondelete
* Add a variable (wgRedactedFunctionArguments) to redact the values sent as certain function
parameters from exception stack traces.
for more details regarding custom functions.
** $wgResourceLoaderLESSImportPaths is an array of file system paths. Files
referenced in LESS '@import' statements are looked up here first.
-* Added meta=filerepoinfo API module for getting information about foreign
- file repositories, and related ForeignAPIRepo methods getInfo and getApiUrl.
+* ResourceLoader supports hashes as module cache invalidation trigger (instead
+ of or in addition to timestamps).
=== Bug fixes in 1.22 ===
* Disable Special:PasswordReset when $wgEnableEmail is false. Previously one
"exists-normalized" instead of filename to be uploaded to.
* (bug 53884) action=edit will now return an error when the specified section
does not exist in the page.
+* Added meta=filerepoinfo API module for getting information about foreign
+ file repositories, and related ForeignAPIRepo methods getInfo and getApiUrl.
=== Languages updated in 1.22===
return $this->language->separatorTransformTable();
}
-
/**
- * Get all the dynamic data for the content language to an array
+ * Get all the dynamic data for the content language to an array.
+ *
+ * NOTE: Before calling this you HAVE to make sure $this->language is set.
*
* @return array
*/
/**
* @param $context ResourceLoaderContext
- * @return array|int|Mixed
+ * @return int: UNIX timestamp
*/
public function getModifiedTime( ResourceLoaderContext $context ) {
- $this->language = Language::factory( $context->getLanguage() );
- $cache = wfGetCache( CACHE_ANYTHING );
- $key = wfMemcKey( 'resourceloader', 'langdatamodule', 'changeinfo' );
+ return max( 1, $this->getHashMtime( $context ) );
+ }
- $data = $this->getData();
- $hash = md5( serialize( $data ) );
+ /**
+ * @param $context ResourceLoaderContext
+ * @return string: Hash
+ */
+ public function getModifiedHash( ResourceLoaderContext $context ) {
+ $this->language = Language::factory( $context->getLanguage() );
- $result = $cache->get( $key );
- if ( is_array( $result ) && $result['hash'] === $hash ) {
- return $result['timestamp'];
- }
- $timestamp = wfTimestamp();
- $cache->set( $key, array(
- 'hash' => $hash,
- 'timestamp' => $timestamp,
- ) );
- return $timestamp;
+ return md5( serialize( $this->getData() ) );
}
/**
* If you want this to happen, you'll need to call getMsgBlobMtime()
* yourself and take its result into consideration.
*
- * @param ResourceLoaderContext $context
- * @return int: UNIX timestamp
+ * NOTE: The mtime of the module's hash is NOT automatically included.
+ * If your module provides a getModifiedHash() method, you'll need to call getHashMtime()
+ * yourself and take its result into consideration.
+ *
+ * @param ResourceLoaderContext $context Context object
+ * @return integer UNIX timestamp
*/
public function getModifiedTime( ResourceLoaderContext $context ) {
// 0 would mean now
return 1;
}
+ /**
+ * Helper method for calculating when the module's hash (if it has one) changed.
+ *
+ * @param ResourceLoaderContext $context
+ * @return integer: UNIX timestamp or 0 if there is no hash provided
+ */
+ public function getHashMtime( ResourceLoaderContext $context ) {
+ $hash = $this->getModifiedHash( $context );
+ if ( !is_string( $hash ) ) {
+ return 0;
+ }
+
+ $cache = wfGetCache( CACHE_ANYTHING );
+ $key = wfMemcKey( 'resourceloader', 'modulemodifiedhash', $this->getName() );
+
+ $data = $cache->get( $key );
+ if ( is_array( $data ) && $data['hash'] === $hash ) {
+ // Hash is still the same, re-use the timestamp of when we first saw this hash.
+ return $data['timestamp'];
+ }
+
+ $timestamp = wfTimestamp();
+ $cache->set( $key, array(
+ 'hash' => $hash,
+ 'timestamp' => $timestamp,
+ ) );
+
+ return $timestamp;
+ }
+
+ /**
+ * Get the last modification timestamp of the message blob for this
+ * module in a given language.
+ *
+ * @param ResourceLoaderContext $context
+ * @return string|null: Hash
+ */
+ public function getModifiedHash( ResourceLoaderContext $context ) {
+ return null;
+ }
+
/**
* Check whether this module is known to be empty. If a child class
* has an easy and cheap way to determine that this module is
--- /dev/null
+<?php
+/**
+ * Send purge requests for pages edited in date range to squid/varnish.
+ *
+ * @section LICENSE
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Maintenance
+ */
+
+require_once __DIR__ . '/Maintenance.php';
+
+/**
+ * Maintenance script that sends purge requests for pages edited in a date
+ * range to squid/varnish.
+ *
+ * Can be used to recover from an HTCP message partition or other major cache
+ * layer interruption.
+ *
+ * @ingroup Maintenance
+ */
+class PurgeChangedPages extends Maintenance {
+
+ public function __construct() {
+ parent::__construct();
+ $this->mDescription = 'Send purge requests for edits in date range to squid/varnish';
+ $this->addOption( 'starttime', 'Starting timestamp', true, true );
+ $this->addOption( 'endtime', 'Ending timestamp', true, true );
+ $this->addOption( 'htcp-dest', 'HTCP announcement destination (IP:port)', false, true );
+ $this->addOption( 'dry-run', 'Do not send purge requests' );
+ $this->addOption( 'verbose', 'Show more output', false, false, 'v' );
+ $this->setBatchSize( 100 );
+ }
+
+ public function execute() {
+ global $wgHTCPRouting;
+
+ if ( $this->hasOption( 'htcp-dest' ) ) {
+ $parts = explode( ':', $this->getOption( 'htcp-dest' ) );
+ if ( count( $parts ) < 2 ) {
+ // Add default htcp port
+ $parts[] = '4827';
+ }
+
+ // Route all HTCP messages to provided host:port
+ $wgHTCPRouting = array(
+ '' => array( 'host' => $parts[0], 'port' => $parts[1] ),
+ );
+ if ( $this->hasOption( 'verbose' ) ) {
+ $this->output( "HTCP broadcasts to {$parts[0]}:{$parts[1]}\n" );
+ }
+ }
+
+ $dbr = $this->getDB( DB_SLAVE );
+ $minTime = $dbr->timestamp( $this->getOption( 'starttime' ) );
+ $maxTime = $dbr->timestamp( $this->getOption( 'endtime' ) );
+
+ if ( $maxTime < $minTime ) {
+ $this->error( "\nERROR: starttime after endtime\n" );
+ $this->maybeHelp( true );
+ }
+
+ $stuckCount = 0; // loop breaker
+ while ( true ) {
+ // Adjust bach size if we are stuck in a second that had many changes
+ $bSize = $this->mBatchSize + ( $stuckCount * $this->mBatchSize );
+
+ $res = $dbr->select(
+ array( 'page', 'revision' ),
+ array(
+ 'rev_timestamp',
+ 'page_namespace',
+ 'page_title',
+ ),
+ array(
+ "rev_timestamp > " . $dbr->addQuotes( $minTime ),
+ "rev_timestamp <= " . $dbr->addQuotes( $maxTime ),
+ // Only get rows where the revision is the latest for the page.
+ // Other revisions would be duplicate and we don't need to purge if
+ // there has been an edit after the interesting time window.
+ "page_latest = rev_id",
+ ),
+ __METHOD__,
+ array( 'ORDER BY' => 'rev_timestamp', 'LIMIT' => $bSize ),
+ array(
+ 'page' => array( 'INNER JOIN', 'rev_page=page_id' ),
+ )
+ );
+
+ if ( !$res->numRows() ) {
+ // nothing more found so we are done
+ break;
+ }
+
+ // Kludge to not get stuck in loops for batches with the same timestamp
+ list( $rows, $lastTime ) = $this->pageableSortedRows( $res, 'rev_timestamp', $bSize );
+ if ( !count( $rows ) ) {
+ ++$stuckCount;
+ continue;
+ }
+ // Reset suck counter
+ $stuckCount = 0;
+
+ $this->output( "Processing changes from {$minTime} to {$lastTime}.\n" );
+
+ // Advance past the last row next time
+ $minTime = $lastTime;
+
+ // Create list of URLs from page_namespace + page_title
+ $urls = array();
+ foreach ( $rows as $row ) {
+ $title = Title::makeTitle( $row->page_namespace, $row->page_title );
+ $urls[] = $title->getInternalURL();
+ }
+
+ if ( $this->hasOption( 'dry-run' ) || $this->hasOption( 'verbose' ) ) {
+ $this->output( implode( "\n", $urls ) . "\n" );
+ if ( $this->hasOption( 'dry-run' ) ) {
+ continue;
+ }
+ }
+
+ // Send batch of purge requests out to squids
+ $squid = new SquidUpdate( $urls );
+ $squid->doUpdate();
+ }
+
+ $this->output( "Done!\n" );
+ }
+
+ /**
+ * Remove all the rows in a result set with the highest value for column
+ * $column unless the number of rows is less $limit. This returns the new
+ * array of rows and the highest value of column $column for the rows left.
+ * The ordering of rows is maintained.
+ *
+ * This is useful for paging on mostly-unique values that may sometimes
+ * have large clumps of identical values. It should be safe to do the next
+ * query on items with a value higher than the highest of the rows returned here.
+ * If this returns an empty array for a non-empty query result, then all the rows
+ * had the same column value and the query should be repeated with a higher LIMIT.
+ *
+ * @TODO: move this elsewhere
+ *
+ * @param ResultWrapper $res Query result sorted by $column (ascending)
+ * @param string $column
+ * @return array (array of rows, string column value)
+ */
+ protected function pageableSortedRows( ResultWrapper $res, $column, $limit ) {
+ $rows = iterator_to_array( $res, false );
+ $count = count( $rows );
+ if ( !$count ) {
+ return array( array(), null ); // nothing to do
+ } elseif ( $count < $limit ) {
+ return array( $rows, $rows[$count - 1]->$column ); // no more rows left
+ }
+ $lastValue = $rows[$count - 1]->$column; // should be the highest
+ for ( $i = $count - 1; $i >= 0; --$i ) {
+ if ( $rows[$i]->$column === $lastValue ) {
+ unset( $rows[$i] );
+ } else {
+ break;
+ }
+ }
+ $lastValueLeft = count( $rows ) ? $rows[count( $rows ) - 1]->$column : null;
+ return array( $rows, $lastValueLeft );
+ }
+}
+
+$maintClass = "PurgeChangedPages";
+require_once RUN_MAINTENANCE_IF_MAIN;
list-style-image: url(@url);
}
-.transition( ... ) {
- -moz-transition: @arguments;
- -webkit-transition: @arguments;
- -o-transition: @arguments;
- transition: @arguments;
+.transition(@string) {
+ -webkit-transition: @string;
+ -moz-transition: @string;
+ -o-transition: @string;
+ transition: @string;
}
}
/* line 36, sourcefiles/scss/components/default/_forms.scss */
.mw-ui-vform > div input:not([type=button]):not([type=submit]):not([type=file]) {
- outline: 0;
border-style: solid;
border-width: 1px;
border-color: #c9c9c9;
color: #252525;
padding: 0.35em 0 0.35em 0.5em;
}
-/* line 12, sourcefiles/scss/mixins/_forms.scss */
+/* line 11, sourcefiles/scss/mixins/_forms.scss */
.mw-ui-vform > div input:not([type=button]):not([type=submit]):not([type=file]):focus {
box-shadow: #4091ed 0px 0px 5px;
border-color: #4091ed;
}
-/* line 38, sourcefiles/scss/components/default/_forms.scss */
+/* line 13, sourcefiles/scss/mixins/_forms.scss */
+.mw-ui-vform > div input:not([type=button]):not([type=submit]):not([type=file]):focus:not([type=checkbox]):not([type=radio]) {
+ outline: 0;
+}
+/* line 40, sourcefiles/scss/components/default/_forms.scss */
.mw-ui-vform > div label {
display: block;
-webkit-box-sizing: border-box;
margin: 0 0 0.2em 0;
padding: 0;
}
-/* line 34, sourcefiles/scss/mixins/_forms.scss */
+/* line 38, sourcefiles/scss/mixins/_forms.scss */
.mw-ui-vform > div label * {
font-weight: normal;
}
-/* line 49, sourcefiles/scss/components/default/_forms.scss */
+/* line 51, sourcefiles/scss/components/default/_forms.scss */
.mw-ui-vform > div input[type="checkbox"],
.mw-ui-vform > div input[type="radio"] {
display: inline;
width: auto;
}
-/* line 65, sourcefiles/scss/components/default/_forms.scss */
+/* line 67, sourcefiles/scss/components/default/_forms.scss */
.mw-ui-input {
- outline: 0;
border-style: solid;
border-width: 1px;
border-color: #c9c9c9;
color: #252525;
padding: 0.35em 0 0.35em 0.5em;
}
-/* line 12, sourcefiles/scss/mixins/_forms.scss */
+/* line 11, sourcefiles/scss/mixins/_forms.scss */
.mw-ui-input:focus {
box-shadow: #4091ed 0px 0px 5px;
border-color: #4091ed;
}
+/* line 13, sourcefiles/scss/mixins/_forms.scss */
+.mw-ui-input:focus:not([type=checkbox]):not([type=radio]) {
+ outline: 0;
+}
-/* line 72, sourcefiles/scss/components/default/_forms.scss */
+/* line 74, sourcefiles/scss/components/default/_forms.scss */
.mw-ui-label {
font-size: 0.9em;
color: #4a4a4a;
}
-/* line 34, sourcefiles/scss/mixins/_forms.scss */
+/* line 38, sourcefiles/scss/mixins/_forms.scss */
.mw-ui-label * {
font-weight: normal;
}
-/* line 81, sourcefiles/scss/components/default/_forms.scss */
+/* line 83, sourcefiles/scss/components/default/_forms.scss */
.mw-ui-checkbox-label, .mw-ui-radio-label {
margin-bottom: 0.5em;
cursor: pointer;
line-height: normal;
font-weight: normal;
}
-/* line 50, sourcefiles/scss/mixins/_forms.scss */
+/* line 54, sourcefiles/scss/mixins/_forms.scss */
.mw-ui-checkbox-label > input[type="checkbox"], .mw-ui-checkbox-label > input[type="radio"], .mw-ui-radio-label > input[type="checkbox"], .mw-ui-radio-label > input[type="radio"] {
width: auto;
height: auto;
}
/* line 36, sourcefiles/scss/components/default/_forms.scss */
.mw-ui-vform > div input:not([type=button]):not([type=submit]):not([type=file]) {
- outline: 0;
border-style: solid;
border-width: 1px;
border-color: #c9c9c9;
color: #252525;
padding: 0.35em 0 0.35em 0.5em;
}
-/* line 12, sourcefiles/scss/mixins/_forms.scss */
+/* line 11, sourcefiles/scss/mixins/_forms.scss */
.mw-ui-vform > div input:not([type=button]):not([type=submit]):not([type=file]):focus {
box-shadow: #4091ed 0px 0px 5px;
border-color: #4091ed;
}
-/* line 38, sourcefiles/scss/components/default/_forms.scss */
+/* line 13, sourcefiles/scss/mixins/_forms.scss */
+.mw-ui-vform > div input:not([type=button]):not([type=submit]):not([type=file]):focus:not([type=checkbox]):not([type=radio]) {
+ outline: 0;
+}
+/* line 40, sourcefiles/scss/components/default/_forms.scss */
.mw-ui-vform > div label {
display: block;
-webkit-box-sizing: border-box;
margin: 0 0 0.2em 0;
padding: 0;
}
-/* line 34, sourcefiles/scss/mixins/_forms.scss */
+/* line 38, sourcefiles/scss/mixins/_forms.scss */
.mw-ui-vform > div label * {
font-weight: normal;
}
-/* line 49, sourcefiles/scss/components/default/_forms.scss */
+/* line 51, sourcefiles/scss/components/default/_forms.scss */
.mw-ui-vform > div input[type="checkbox"],
.mw-ui-vform > div input[type="radio"] {
display: inline;
width: auto;
}
-/* line 65, sourcefiles/scss/components/default/_forms.scss */
+/* line 67, sourcefiles/scss/components/default/_forms.scss */
.mw-ui-input {
- outline: 0;
border-style: solid;
border-width: 1px;
border-color: #c9c9c9;
color: #252525;
padding: 0.35em 0 0.35em 0.5em;
}
-/* line 12, sourcefiles/scss/mixins/_forms.scss */
+/* line 11, sourcefiles/scss/mixins/_forms.scss */
.mw-ui-input:focus {
box-shadow: #4091ed 0px 0px 5px;
border-color: #4091ed;
}
+/* line 13, sourcefiles/scss/mixins/_forms.scss */
+.mw-ui-input:focus:not([type=checkbox]):not([type=radio]) {
+ outline: 0;
+}
-/* line 72, sourcefiles/scss/components/default/_forms.scss */
+/* line 74, sourcefiles/scss/components/default/_forms.scss */
.mw-ui-label {
font-size: 0.9em;
color: #4a4a4a;
}
-/* line 34, sourcefiles/scss/mixins/_forms.scss */
+/* line 38, sourcefiles/scss/mixins/_forms.scss */
.mw-ui-label * {
font-weight: normal;
}
-/* line 81, sourcefiles/scss/components/default/_forms.scss */
+/* line 83, sourcefiles/scss/components/default/_forms.scss */
.mw-ui-checkbox-label, .mw-ui-radio-label {
margin-bottom: 0.5em;
cursor: pointer;
line-height: normal;
font-weight: normal;
}
-/* line 50, sourcefiles/scss/mixins/_forms.scss */
+/* line 54, sourcefiles/scss/mixins/_forms.scss */
.mw-ui-checkbox-label > input[type="checkbox"], .mw-ui-checkbox-label > input[type="radio"], .mw-ui-radio-label > input[type="checkbox"], .mw-ui-radio-label > input[type="radio"] {
width: auto;
height: auto;
// Font is not included.
// For Vector, that should be layered on top with vector-type
@mixin agora-field-styling() {
- @include reset-focus; // Removes OS field focus
-
- border: {
- style: solid;
- width: 1px;
- color: $agoraGray;
- };
-
- &:focus {
- // @include box-shadow generates unneeded prefixes
- // https://github.com/chriseppstein/compass/issues/1054 , so specify
- // directly.
- box-shadow: $agoraBlueShadow 0px 0px 5px;
-
- border: {
- color: $agoraBlueShadow;
- };
- }
-
- color: $agoraTextColor;
- padding: 0.35em 0 0.35em 0.5em;
+
+ border: {
+ style: solid;
+ width: 1px;
+ color: $agoraGray;
+ };
+
+ &:focus {
+ // Styling focus of native checkboxes etc on Mac is almost impossible.
+ &:not([type=checkbox]):not([type=radio]) {
+ @include reset-focus; // Removes OS field focus
+ };
+
+ // @include box-shadow generates unneeded prefixes
+ // https://github.com/chriseppstein/compass/issues/1054 , so specify
+ // directly.
+ box-shadow: $agoraBlueShadow 0px 0px 5px;
+
+ border: {
+ color: $agoraBlueShadow;
+ };
+ }
+
+ color: $agoraTextColor;
+ padding: 0.35em 0 0.35em 0.5em;
}
@mixin agora-label-styling() {
- font: {
- //weight: bold;
- size: 0.9em;
- };
- color: darken($agoraGray, 50%);
-
- & * {
- font-weight: normal;
- }
+ font: {
+ //weight: bold;
+ size: 0.9em;
+ };
+ color: darken($agoraGray, 50%);
+
+ & * {
+ font-weight: normal;
+ }
}
@mixin agora-inline-label-styling() {
- margin-bottom: 0.5em;
- cursor: pointer;
- vertical-align: bottom;
- line-height: normal;
-
- font: {
- weight: normal;
- };
-
- & > input[type="checkbox"],
- & > input[type="radio"] {
- width: auto;
- height: auto;
- margin: 0 0.1em 0em 0;
- padding: 0;
- border: {
- style: solid;
- width: 1px;
- color: $agoraGray;
- }
- cursor: pointer;
- }
+ margin-bottom: 0.5em;
+ cursor: pointer;
+ vertical-align: bottom;
+ line-height: normal;
+
+ font: {
+ weight: normal;
+ };
+
+ & > input[type="checkbox"],
+ & > input[type="radio"] {
+ width: auto;
+ height: auto;
+ margin: 0 0.1em 0em 0;
+ padding: 0;
+ border: {
+ style: solid;
+ width: 1px;
+ color: $agoraGray;
+ }
+ cursor: pointer;
+ }
}
* @class mw.Title
*
* Parse titles into an object struture. Note that when using the constructor
- * directly, passing invalid titles will result in an exception. Use
- * #newFromText to use the logic directly and get null for invalid titles
- * which is easier to work with.
+ * directly, passing invalid titles will result in an exception. Use #newFromText to use the
+ * logic directly and get null for invalid titles which is easier to work with.
*
* @constructor
* @param {string} title Title of the page. If no second argument given,
div#content,
div#footer,
#left-navigation {
- .transition( margin-left 250ms, padding 250ms );
+ .transition( margin-left 250ms, padding 250ms; );
}
#p-logo {
- .transition( 'left 250ms' );
+ .transition( left 250ms );
}
#mw-panel {
.transition( padding-right 250ms );
if ( $handler && $fileNamePos !== false ) {
$paramString = substr( $thumbname, 0, $fileNamePos - 1 );
$extraParams = $handler->parseParamString( $paramString );
- if ( $handler !== false ) {
+ if ( $extraParams !== false ) {
return $params + $extraParams;
}
}