Add licensing for extensions to Special:Version
authorMatt Walker <mwalker@wikimedia.org>
Fri, 24 May 2013 11:19:49 +0000 (04:19 -0700)
committerMatt Walker <mwalker@wikimedia.org>
Fri, 3 Jan 2014 19:02:54 +0000 (11:02 -0800)
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
includes/api/ApiQuerySiteinfo.php
includes/specials/SpecialVersion.php
languages/messages/MessagesEn.php
languages/messages/MessagesQqq.php
maintenance/language/messages.inc

diff --git a/CREDITS b/CREDITS
index 01505b0..21db850 100644 (file)
--- a/CREDITS
+++ b/CREDITS
@@ -1,8 +1,11 @@
+{{int:version-credits-summary}}
+<!--
 MediaWiki 1.23 is a collaborative project released under the
 GNU General Public License v2. We would like to recognize the
 following names for their contribution to the product.
-
+-->
 <!-- Please notice that the following can be found parsed under Special:Version/Credits -->
+
 == Developers ==
 * Aaron Schulz
 * Alex Z.
index a94f5bb..5e9cd08 100644 (file)
@@ -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;
                        }
                }
index 5ba785f..2465620 100644 (file)
@@ -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 = "<pre>$wikiText</pre>";
+                                               }
+                                       }
+                               }
 
-                       $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 .= "<br/>" . $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 = '&ndash;';
+               $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 .= "<br/>" . $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 = '<span class="mw-version-ext-version">' .
-                               $this->msg( 'version-version', $extension['version'] )->text() .
-                               '</span>';
-               } 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( <msgname>, <parameter>, <parameter>... ) 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 = "<tr>
-                               <td><em>$mainLink $versionText</em></td>
-                               <td><em>$vcsText</em></td>";
+               } elseif ( isset( $extension['description'] ) ) {
+                       // Non localized version
+                       $description = $out->parseInline( $extension['description'] );
                } else {
-                       $extNameVer = "<tr>
-                               <td colspan=\"2\"><em>$mainLink $versionText</em></td>";
+                       $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 = "<td>$description</td>
-                       <td>" . $this->listAuthors( $author, false ) . "</td>
-                       </tr>\n";
+               $html .= Html::rawElement( 'td', array(), '<b>' . $extensionNameLink . '</b><br />' . $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.
         *
index f3e1fb4..d80ec61 100644 (file)
@@ -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]].',
index e3e574a..9ae5a0f 100644 (file)
@@ -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}}
index aeb9453..0d990b6 100644 (file)
@@ -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',