From 357eb3d488b9d8caf575bc6990d9906ecc256554 Mon Sep 17 00:00:00 2001 From: Matt Walker Date: Fri, 24 May 2013 04:19:49 -0700 Subject: [PATCH] Add licensing for extensions to Special:Version Allow extensions to register a software license and present this on the Special:Version page. A new $wgExtensionCredits parameter has been introduced for this purpose: 'license-name'. This will also automatically pick up the presense of additional licensing and/or credits files. If ((AUTHORS)|(CREDITS))(\.txt)? exists in the extension base directory a credits link will be created. If ((LICENSE)|(COPYING))(\.txt)? exists a license link will be created. The API has also been updated to produce VCS information and present links to the license/credits files. Bug: 48418 Change-Id: I388f3b630462f1909f30751c987f7af585e98881 --- CREDITS | 5 +- includes/api/ApiQuerySiteinfo.php | 25 ++ includes/specials/SpecialVersion.php | 363 +++++++++++++++++++++------ languages/messages/MessagesEn.php | 13 +- languages/messages/MessagesQqq.php | 7 +- maintenance/language/messages.inc | 5 + 6 files changed, 335 insertions(+), 83 deletions(-) diff --git a/CREDITS b/CREDITS index 01505b0af6..21db8509b0 100644 --- a/CREDITS +++ b/CREDITS @@ -1,8 +1,11 @@ +{{int:version-credits-summary}} + + == Developers == * Aaron Schulz * Alex Z. diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php index a94f5bbf0a..5e9cd08027 100644 --- a/includes/api/ApiQuerySiteinfo.php +++ b/includes/api/ApiQuerySiteinfo.php @@ -547,6 +547,31 @@ class ApiQuerySiteinfo extends ApiQueryBase { ) { $ret['version'] = 'r' . $m[1]; } + if ( isset( $ext['path'] ) ) { + $extensionPath = dirname( $ext['path'] ); + $gitInfo = new GitInfo( $extensionPath ); + $vcsVersion = $gitInfo->getHeadSHA1(); + if ( $vcsVersion !== false ) { + $ret['vcs-system'] = 'git'; + $ret['vcs-version'] = $vcsVersion; + $ret['vcs-url'] = $gitInfo->getHeadViewUrl(); + $ret['vcs-date'] = wfTimestamp( TS_ISO_8601, $gitInfo->getHeadCommitDate() ); + } else { + $svnInfo = SpecialVersion::getSvnInfo( $extensionPath ); + if ( $svnInfo !== false ) { + $ret['vcs-system'] = 'svn'; + $ret['vcs-version'] = $svnInfo['checkout-rev']; + $ret['vcs-url'] = isset( $svnInfo['viewvc-url'] ) ? $svnInfo['viewvc-url'] : ''; + } + } + if ( SpecialVersion::getExtLicenseFileName( $extensionPath ) ) { + $ret['license-name'] = isset( $ext['license-name'] ) ? $ext['license-name'] : ''; + $ret['license'] = SpecialPage::getTitleFor( 'Version', "License/{$ext['name']}" )->getLinkURL(); + } + if ( SpecialVersion::getExtAuthorsFileName( $extensionPath ) ) { + $ret['credits'] = SpecialPage::getTitleFor( 'Version', "Credits/{$ext['name']}" )->getLinkURL(); + } + } $data[] = $ret; } } diff --git a/includes/specials/SpecialVersion.php b/includes/specials/SpecialVersion.php index 5ba785f57d..2465620246 100644 --- a/includes/specials/SpecialVersion.php +++ b/includes/specials/SpecialVersion.php @@ -48,41 +48,86 @@ class SpecialVersion extends SpecialPage { * main() */ public function execute( $par ) { - global $wgSpecialVersionShowHooks, $IP; + global $wgSpecialVersionShowHooks, $IP, $wgExtensionCredits; $this->setHeaders(); $this->outputHeader(); $out = $this->getOutput(); $out->allowClickjacking(); - if ( $par !== 'Credits' ) { - $text = - $this->getMediaWikiCredits() . - $this->softwareInformation() . - $this->getEntryPointInfo() . - $this->getExtensionCredits(); - if ( $wgSpecialVersionShowHooks ) { - $text .= $this->getWgHooks(); + // Explode the sub page information into useful bits + $parts = explode( '/', (string)$par ); + if ( isset( $parts[1] ) ) { + $extName = str_replace( '_', ' ', $parts[1] ); + $extNode = null; + // Find it! + foreach ( $wgExtensionCredits as $group => $extensions ) { + foreach ( $extensions as $ext ) { + if ( isset( $ext['name'] ) && ( $ext['name'] === $extName ) ) { + $extNode = &$ext; + break 2; + } + } } - - $out->addWikiText( $text ); - $out->addHTML( $this->IPInfo() ); - - if ( $this->getRequest()->getVal( 'easteregg' ) ) { - // TODO: put something interesting here + if ( !$extNode ) { + $out->setStatusCode( 404 ); } } else { - // Credits sub page + $extName = 'MediaWiki'; + } - // Header - $out->addHTML( wfMessage( 'version-credits-summary' )->parseAsBlock() ); + // Now figure out what to do + switch ( strtolower( $parts[0] ) ) { + case 'credits': + $wikiText = '{{int:version-credits-not-found}}'; + if ( $extName === 'MediaWiki' ) { + $wikiText = file_get_contents( $IP . '/CREDITS' ); + } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) { + $file = $this->getExtAuthorsFileName( dirname( $extNode['path'] ) ); + if ( $file ) { + $wikiText = file_get_contents( $file ); + } + } + + $out->setPageTitle( $this->msg( 'version-credits-title', $extName ) ); + $out->addWikiText( $wikiText ); + break; + + case 'license': + $wikiText = '{{int:version-license-not-found}}'; + if ( $extName === 'MediaWiki' ) { + $wikiText = file_get_contents( $IP . '/COPYING' ); + } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) { + $file = $this->getExtLicenseFileName( dirname( $extNode['path'] ) ); + if ( $file ) { + $wikiText = file_get_contents( $file ); + if ( !isset( $extNode['license-name'] ) ) { + // If the developer did not explicitly set license-name they probably + // are unaware that we're now sucking this file in and thus it's probably + // not wikitext friendly. + $wikiText = "
$wikiText
"; + } + } + } - $wikiText = file_get_contents( $IP . '/CREDITS' ); + $out->setPageTitle( $this->msg( 'version-license-title', $extName ) ); + $out->addWikiText( $wikiText ); + break; + + default: + $out->addWikiText( + $this->getMediaWikiCredits() . + $this->softwareInformation() . + $this->getEntryPointInfo() + ); + $out->addHtml( $this->getExtensionCredits() ); + if ( $wgSpecialVersionShowHooks ) { + $out->addWikiText( $this->getWgHooks() ); + } - // Take everything from the first section onwards, to remove the (not localized) header - $wikiText = substr( $wikiText, strpos( $wikiText, '==' ) ); + $out->addHTML( $this->IPInfo() ); - $out->addWikiText( $wikiText ); + break; } } @@ -298,7 +343,7 @@ class SpecialVersion extends SpecialPage { $gitHeadCommitDate = $gitInfo->getHeadCommitDate(); if ( $gitHeadCommitDate ) { - $shortSHA1 .= "
" . $wgLang->timeanddate( $gitHeadCommitDate, true ); + $shortSHA1 .= Html::element( 'br' ) . $wgLang->timeanddate( $gitHeadCommitDate, true ); } return self::getwgVersionLinked() . " $shortSHA1"; @@ -460,63 +505,120 @@ class SpecialVersion extends SpecialPage { } /** - * Creates and formats the credits for a single extension and returns this. + * Creates and formats a version line for a single extension. + * + * Information for four columns will be created. Parameters required in the + * $extension array for part rendering are indicated in () + * - The name of (name), and URL link to (url), the extension + * -- Also if available the short name of the license (license-name) and a linke + * to ((LICENSE)|(COPYING))(\.txt)? if it exists. + * - Official version number (version) and if available version control system + * revision (path), link, and date + * - Description of extension (descriptionmsg or description) + * - List of authors (author) and link to a ((AUTHORS)|(CREDITS))(\.txt)? file if it exists * * @param $extension Array * - * @return string + * @return string raw HTML */ function getCreditsForExtension( array $extension ) { - global $wgLang; + $out = $this->getOutput(); + + // We must obtain the information for all the bits and pieces! + // ... such as extension names and links + $extensionName = isset( $extension['name'] ) ? $extension['name'] : '[no name]'; + if ( isset( $extension['url'] ) ) { + $extensionNameLink = Linker::makeExternalLink( + $extension['url'], + $extensionName, + true, + '', + array( 'class' => 'mw-version-ext-name' ) + ); + } else { + $extensionNameLink = $extensionName; + } - $name = isset( $extension['name'] ) ? $extension['name'] : '[no name]'; + // ... and the version information + // If the extension path is set we will check that directory for GIT and SVN + // metadata in an attempt to extract date and vcs commit metadata. + $canonicalVersion = '–'; + $extensionPath = null; + $vcsVersion = null; + $vcsLink = null; + $vcsDate = null; - $vcsText = false; + if ( isset( $extension['version'] ) ) { + $canonicalVersion = $out->parseInline( $extension['version'] ); + } if ( isset( $extension['path'] ) ) { - $gitInfo = new GitInfo( dirname( $extension['path'] ) ); - $gitHeadSHA1 = $gitInfo->getHeadSHA1(); - if ( $gitHeadSHA1 !== false ) { - $vcsText = '(' . substr( $gitHeadSHA1, 0, 7 ) . ')'; - $gitViewerUrl = $gitInfo->getHeadViewUrl(); - if ( $gitViewerUrl !== false ) { - $vcsText = "[$gitViewerUrl $vcsText]"; - } - $gitHeadCommitDate = $gitInfo->getHeadCommitDate(); - if ( $gitHeadCommitDate ) { - $vcsText .= "
" . $wgLang->timeanddate( $gitHeadCommitDate, true ); - } + $extensionPath = dirname( $extension['path'] ); + $gitInfo = new GitInfo( $extensionPath ); + $vcsVersion = $gitInfo->getHeadSHA1(); + if ( $vcsVersion !== false ) { + $vcsVersion = substr( $vcsVersion, 0, 7 ); + $vcsLink = $gitInfo->getHeadViewUrl(); + $vcsDate = $gitInfo->getHeadCommitDate(); } else { - $svnInfo = self::getSvnInfo( dirname( $extension['path'] ) ); - # Make subversion text/link. + $svnInfo = self::getSvnInfo( $extensionPath ); if ( $svnInfo !== false ) { - $directoryRev = isset( $svnInfo['directory-rev'] ) ? $svnInfo['directory-rev'] : null; - $vcsText = $this->msg( 'version-svn-revision', $directoryRev, $svnInfo['checkout-rev'] )->text(); - $vcsText = isset( $svnInfo['viewvc-url'] ) ? '[' . $svnInfo['viewvc-url'] . " $vcsText]" : $vcsText; + $vcsVersion = $this->msg( 'version-svn-revision', $svnInfo['checkout-rev'] )->text(); + $vcsLink = isset( $svnInfo['viewvc-url'] ) ? $svnInfo['viewvc-url'] : ''; } } } - # Make main link (or just the name if there is no URL). - if ( isset( $extension['url'] ) ) { - $mainLink = "[{$extension['url']} $name]"; - } else { - $mainLink = $name; + $versionString = Html::rawElement( 'span', array( 'class' => 'mw-version-ext-version' ), $canonicalVersion ); + if ( $vcsVersion ) { + if ( $vcsLink ) { + $vcsVerString = Linker::makeExternalLink( + $vcsLink, + $this->msg( 'version-version', $vcsVersion ), + true, + '', + array( 'class' => 'mw-version-ext-vcs-version' ) + ); + } else { + $vcsVerString = Html::element( 'span', + array( 'class' => 'mw-version-ext-vcs-version'), + "({$vcsVersion})" + ); + } + $versionString .= " {$vcsVerString}"; + + if ( $vcsDate ) { + $vcsTimeString = Html::element( 'br' ) . + Html::element( 'span', + array( 'class' => 'mw-version-ext-vcs-timestamp'), + $this->getLanguage()->timeanddate( $vcsDate ) + ); + $versionString .= " {$vcsTimeString}"; + } } - if ( isset( $extension['version'] ) ) { - $versionText = '' . - $this->msg( 'version-version', $extension['version'] )->text() . - ''; - } else { - $versionText = ''; + // ... and license information; if a license file exists we + // will link to it + $licenseLink = ''; + if ( isset( $extension['license-name'] ) ) { + $licenseLink = Linker::link( + $this->getTitle( 'License/' . $extensionName ), + $out->parseInline( $extension['license-name'] ), + array( 'class' => 'mw-version-ext-license' ) + ); + } elseif ( $this->getExtLicenseFileName( $extensionPath ) ) { + $licenseLink = Linker::link( + $this->getTitle( 'License/' . $extensionName ), + $this->msg( 'version-ext-license' ), + array( 'class' => 'mw-version-ext-license' ) + ); } - # Make description text. - $description = isset( $extension['description'] ) ? $extension['description'] : ''; - + // ... and generate the description; which can be a parameterized l10n message + // in the form array( , , ... ) or just a straight + // up string if ( isset( $extension['descriptionmsg'] ) ) { - # Look for a localized description. + // Localized description of extension $descriptionMsg = $extension['descriptionmsg']; if ( is_array( $descriptionMsg ) ) { @@ -527,23 +629,33 @@ class SpecialVersion extends SpecialPage { } else { $description = $this->msg( $descriptionMsg )->text(); } - } - - if ( $vcsText !== false ) { - $extNameVer = " - $mainLink $versionText - $vcsText"; + } elseif ( isset( $extension['description'] ) ) { + // Non localized version + $description = $out->parseInline( $extension['description'] ); } else { - $extNameVer = " - $mainLink $versionText"; + $description = ''; } + $description = $out->parseInline( $description ); + + // ... now get the authors for this extension + $authors = isset( $extension['author'] ) ? $extension['author'] : array(); + $authors = $this->listAuthors( $authors, $extensionName, $extensionPath ); + + // Finally! Create the table + $html = Html::openElement( 'tr', array( + 'class' => 'mw-version-ext', + 'id' => "mw-version-ext-{$extensionName}" + ) + ); - $author = isset( $extension['author'] ) ? $extension['author'] : array(); - $extDescAuthor = "$description - " . $this->listAuthors( $author, false ) . " - \n"; + $html .= Html::rawElement( 'td', array(), '' . $extensionNameLink . '
' . $versionString ); + $html .= Html::rawElement( 'td', array(), $licenseLink ); + $html .= Html::rawElement( 'td', array( 'class' => 'mw-version-ext-description' ), $description ); + $html .= Html::rawElement( 'td', array( 'class' => 'mw-version-ext-authors' ), $authors ); - return $extNameVer . $extDescAuthor; + $html .= Html::closeElement( 'td' ); + + return $html; } /** @@ -611,23 +723,120 @@ class SpecialVersion extends SpecialPage { /** * Return a formatted unsorted list of authors * + * 'And Others' + * If an item in the $authors array is '...' it is assumed to indicate an + * 'and others' string which will then be linked to an ((AUTHORS)|(CREDITS))(\.txt)? + * file if it exists in $dir. + * + * Similarly an entry ending with ' ...]' is assumed to be a link to an + * 'and others' page. + * + * If no '...' string variant is found, but an authors file is found an + * 'and others' will be added to the end of the credits. + * * @param $authors mixed: string or array of strings + * @param $extName string: name of the extension for link creation + * @param $extDir string: path to the extension root directory + * * @return String: HTML fragment */ - function listAuthors( $authors ) { + function listAuthors( $authors, $extName, $extDir ) { + $hasOthers = false; + $list = array(); foreach ( (array)$authors as $item ) { if ( $item == '...' ) { - $list[] = $this->msg( 'version-poweredby-others' )->text(); + $hasOthers = true; + + if ( $this->getExtAuthorsFileName( $extDir ) ) { + $text = Linker::link( + $this->getTitle( "Credits/$extName" ), + $this->msg( 'version-poweredby-others' )->text() + ); + } else { + $text = $this->msg( 'version-poweredby-others' )->text(); + } + $list[] = $text; + } elseif ( substr( $item, -5 ) == ' ...]' ) { - $list[] = substr( $item, 0, -4 ) . $this->msg( 'version-poweredby-others' )->text() . "]"; + $hasOthers = true; + $list[] = $this->getOutput()->parseInline( + substr( $item, 0, -4 ) . $this->msg( 'version-poweredby-others' )->text() . "]" + ); + } else { - $list[] = $item; + $list[] = $this->getOutput()->parseInline( $item ); } } + + if ( !$hasOthers && $this->getExtAuthorsFileName( $extDir ) ) { + $list[] = $text = Linker::link( + $this->getTitle( "Credits/$extName" ), + $this->msg( 'version-poweredby-others' )->text() + ); + } + return $this->listToText( $list, false ); } + /** + * Obtains the full path of an extensions authors or credits file if + * one exists. + * + * @param string $extDir: Path to the extensions root directory + * + * @since 1.23 + * + * @return bool|string False if no such file exists, otherwise returns + * a path to it. + */ + public static function getExtAuthorsFileName( $extDir ) { + if ( !$extDir ) { + return false; + } + + foreach ( scandir( $extDir ) as $file ) { + $fullPath = $extDir . DIRECTORY_SEPARATOR . $file; + if ( preg_match( '/^((AUTHORS)|(CREDITS))(\.txt)?$/', $file ) && + is_readable( $fullPath ) && + is_file( $fullPath ) + ) { + return $fullPath; + } + } + + return false; + } + + /** + * Obtains the full path of an extensions copying or license file if + * one exists. + * + * @param string $extDir: Path to the extensions root directory + * + * @since 1.23 + * + * @return bool|string False if no such file exists, otherwise returns + * a path to it. + */ + public static function getExtLicenseFileName( $extDir ) { + if ( !$extDir ) { + return false; + } + + foreach ( scandir( $extDir ) as $file ) { + $fullPath = $extDir . DIRECTORY_SEPARATOR . $file; + if ( preg_match( '/^((COPYING)|(LICENSE))(\.txt)?$/', $file ) && + is_readable( $fullPath ) && + is_file( $fullPath ) + ) { + return $fullPath; + } + } + + return false; + } + /** * Convert an array of items into a list for display. * diff --git a/languages/messages/MessagesEn.php b/languages/messages/MessagesEn.php index f3e1fb484d..d80ec61b13 100644 --- a/languages/messages/MessagesEn.php +++ b/languages/messages/MessagesEn.php @@ -4813,10 +4813,15 @@ You can also [[Special:EditWatchlist|use the standard editor]].', 'version-parser-function-hooks' => 'Parser function hooks', 'version-hook-name' => 'Hook name', 'version-hook-subscribedby' => 'Subscribed by', -'version-version' => '(Version $1)', -'version-svn-revision' => '(r$2)', # only translate this message to other languages if you have to change it -'version-license' => 'License', -'version-poweredby-credits' => "This wiki is powered by '''[https://www.mediawiki.org/ MediaWiki]''', copyright © 2001-$1 $2.", +'version-version' => '($1)', +'version-svn-revision' => 'r$1', # only translate this message to other languages if you have to change it +'version-license' => 'MediaWiki License', +'version-license-title' => 'License for $1', +'version-license-not-found' => 'No detailed license information was found for this extension.', +'version-credits-title' => 'Credits for $1', +'version-credits-not-found' => 'No detailed credits information was found for this extension.', +'version-ext-license' => 'License', +'version-poweredby-credits' => "This wiki is powered by '''[//www.mediawiki.org/ MediaWiki]''', copyright © 2001-$1 $2.", 'version-poweredby-others' => 'others', 'version-poweredby-translators' => 'translatewiki.net translators', 'version-credits-summary' => 'We would like to recognize the following persons for their contribution to [[Special:Version|MediaWiki]].', diff --git a/languages/messages/MessagesQqq.php b/languages/messages/MessagesQqq.php index e3e574a499..9ae5a0f20b 100644 --- a/languages/messages/MessagesQqq.php +++ b/languages/messages/MessagesQqq.php @@ -9928,7 +9928,7 @@ Parameters: Used in [[Special:Version]], preceeding the Subversion revision numbers of the extensions loaded inside brackets, like this: "({{int:version-revision}} r012345"). Parameters: * $1 - (Unused) directory revision number or empty string * $2 - checkout revision number', -'version-license' => '{{Identical|License}}', +'version-license' => '{{Identical|License}} but used specifically for the MediaWiki software', 'version-poweredby-credits' => 'Message shown on [[Special:Version]]. Parameters: * $1 - the current year * $2 - a list of selected MediaWiki authors', @@ -9954,6 +9954,11 @@ See also {{msg-mw|Version-entrypoints}}', A short description of the article path entry point. Links to the mediawiki.org documentation page for $wgArticlePath.', 'version-entrypoints-scriptpath' => '{{Optional}} A short description of the script path entry point. Links to the mediawiki.org documentation page for $wgScriptPath.', +'version-license-title' => 'Page title for an extended license for a piece of software. Argument $1 is the name of software.', +'version-license-not-found' => 'Descriptive error used when detailed license text for a piece of software is not found', +'version-credits-title' => 'Page title for an about/credits page for a MediaWiki extension. $1 is the name of the extension.', +'version-credits-not-found' => 'Descriptive error used when detailed about/credits for an extension are not available.', +'version-ext-license' => '{{Identical|License}}', # Special:Redirect 'redirect' => "{{doc-special|Redirect}} diff --git a/maintenance/language/messages.inc b/maintenance/language/messages.inc index aeb9453994..0d990b60ab 100644 --- a/maintenance/language/messages.inc +++ b/maintenance/language/messages.inc @@ -3659,6 +3659,11 @@ $wgMessageStructure = array( 'version-version', 'version-svn-revision', 'version-license', + 'version-ext-license', + 'version-license-title', + 'version-license-not-found', + 'version-credits-title', + 'version-credits-not-found', 'version-poweredby-credits', 'version-poweredby-others', 'version-poweredby-translators', -- 2.20.1