Brian Wolff has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/290963

Change subject: Separate out List content from Hub content
......................................................................

Separate out List content from Hub content

Also adds better validation of json content. Has a
rudiementry display of lists, that's just for the moment,
I assume we'll change that part later.

List content when transcluded will only show the first 5 items.

Bug: T135088
Change-Id: I343a374ff00d40f84a3bdabdaee28b8f6d27c5a4
---
M extension.json
M i18n/en.json
M i18n/qqq.json
A includes/content/CollaborationListContent.php
A includes/content/CollaborationListContentHandler.php
5 files changed, 437 insertions(+), 3 deletions(-)


  git pull 
ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/CollaborationKit 
refs/changes/63/290963/1

diff --git a/extension.json b/extension.json
index b88dbe1..13dc1cb 100644
--- a/extension.json
+++ b/extension.json
@@ -20,11 +20,14 @@
                "CollaborationKitHooks": "includes/CollaborationKit.php",
                "CollaborationHubContent": 
"includes/content/CollaborationHubContent.php",
                "CollaborationHubContentHandler": 
"includes/content/CollaborationHubContentHandler.php",
+               "CollaborationListContent": 
"includes/content/CollaborationListContent.php",
+               "CollaborationListContentHandler": 
"includes/content/CollaborationListContentHandler.php",
                "SpecialCreateCollaborationHub": 
"includes/SpecialCreateCollaborationHub.php",
                "SpecialEditCollaborationHub": 
"includes/SpecialEditCollaborationHub.php"
        },
        "ContentHandlers": {
-               "CollaborationHubContent": "CollaborationHubContentHandler"
+               "CollaborationHubContent": "CollaborationHubContentHandler",
+               "CollaborationListContent": "CollaborationListContentHandler"
        },
        "SpecialPages": {
                "EditCollaborationHub": "SpecialEditCollaborationHub",
diff --git a/i18n/en.json b/i18n/en.json
index 23c972a..4ce1daf 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -36,5 +36,7 @@
        "collaborationhub-display-list": "Simple list",
        "collaborationkit-edit-missingsummary": "Missing edit summary",
        "collaborationhub-edit-apierror": "Editing the Collaboration Hub 
through the API failed with error code $1.",
-       "collaborationhub-edit-tojsonerror": "Error creating json out of 
provided junk"
+       "collaborationhub-edit-tojsonerror": "Error creating json out of 
provided junk",
+       "collaborationkit-taglist": "'''{{PLURAL:$2|Tagged:|Tags:}}''' $1 ",
+       "collaborationkit-listempty": "This list has no items in it."
 }
diff --git a/i18n/qqq.json b/i18n/qqq.json
index ae4242a..72a07bd 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -36,5 +36,7 @@
        "collaborationhub-display-list": "Select option for a plain list",
        "collaborationkit-edit-missingsummary": "Warning that the edit summary 
is missing on Special:EditCollaborationHub (maybe should be worded more/less 
harshly depending on how/when it actually shows up)\n\nApparently refers to 
{{msg-mw|Htmlform-submit}}",
        "collaborationhub-edit-apierror": "Error message shown when a request 
to the edit API to update a collaboration hub failed.\n\nParameters:\n* $1 - 
error code string",
-       "collaborationhub-edit-tojsonerror": "Error message shown when the 
input could not be successfully encoded back into the collaboration hub json"
+       "collaborationhub-edit-tojsonerror": "Error message shown when the 
input could not be successfully encoded back into the collaboration hub json",
+       "collaborationkit-taglist": "Box for showing tags a specific item has 
on a list. $1 = comma separated list of tags. $2 = number of tags",
+       "collaborationkit-listempty": "Shown on lists that are empty 
immediately after the description"
 }
diff --git a/includes/content/CollaborationListContent.php 
b/includes/content/CollaborationListContent.php
new file mode 100644
index 0000000..8c7e49e
--- /dev/null
+++ b/includes/content/CollaborationListContent.php
@@ -0,0 +1,351 @@
+<?php
+
+/**
+ *
+ * Json structure is as follows:
+ * {
+ *     items: [
+ *             {
+ *                     "title": "The title, possibly a page name",
+ *                     "link": "Page to link to if not title, or false to 
disable link",
+ *                     "notes": "Freeform wikitext",
+ *                     "image": "Image to use. Set to false to disable using 
PageImages ext",
+ *                     "sortkey": { "name of sort criteria": "sortkey", ... }
+ *                     "tags": [ "tag1", "tag2", ... ]
+ *             },
+ *             ...
+ *     ],
+ *     "options": {
+ *             "sortcriteria": {
+ *                     // FIXME, not sure if this actually meets with usecase
+ *                     "criteria name": {
+ *                             "order": "numeric", // or potentially 
collation??
+ *                             "default": "string-Text here" // or 
"column-title", etc
+ *                     },
+ *                     ...
+ *             },
+ *             "defaultsort": "sort-criteria name" // or "column-title" ?? 
what about multi-key sort??
+ *     }
+ *     "description": "Some arbitrary wikitext"
+ *}
+ *
+ */
+class CollaborationListContent extends JsonContent {
+
+       const MAX_LIST_SIZE = 2000; // Maybe should be a config option.
+
+       /** @var $decoded boolean Have we decoded the data yet */
+       private $decoded = false;
+       /** @var $description String Descripton wikitext */
+       protected $description;
+       /** @var $options StdClass Options for page */
+       protected $options;
+       /** @var $items Array List of items */
+       protected $items;
+
+       function __construct( $text, $type = 'CollaborationListContent' ) {
+               parent::__construct( $text, $type );
+       }
+
+       /**
+        * Decode and validate the contents.
+        * @return bool Whether the contents are valid
+        */
+       public function isValid() {
+               if ( !parent::isValid() ) {
+                       return false;
+               }
+
+               $status = $this->getData();
+               if ( !is_object( $status ) || !$status->isOk() ) {
+                       return false;
+               }
+
+               $data = $status->value;
+
+               if (
+                       !property_exists( $data, "items" )
+                       || !property_exists( $data, "options" )
+                       || !property_exists( $data, "description" )
+               ) { 
+                       return false;
+               }
+               foreach( $data as $field => $value ) {
+                       switch( $field ) {
+                       case 'items':
+                               if( !is_array( $value ) ) { 
+                                       return false;
+                               }
+                               if( !$this->validateItems( $value ) ) {
+                                       return false;
+                               }
+                               break;
+                       case 'options':
+                               if( !is_object( $value ) ) {
+                                       return false;
+                               }
+                               if( !$this->validateOptions( $value ) ) {
+                                       return false;
+                               }
+                               break;
+                       case 'description':
+                               if( !is_string( $value ) ) {
+                                       return false;
+                               }
+                               break;
+                       default:
+                               return false;
+                       }
+               }
+               return true;
+       }
+
+       /**
+        * Validate the item structure.
+        *
+        * Format is a list of:
+        *      {
+        *              "title": "The title, possibly but not neccessarily a 
page name",
+        *              "link": "Page to link to if not title, or false to 
disable link",
+        *              "notes": "Freeform wikitext",
+        *              "image": "Image to use. Set to false to disable using 
PageImages ext",
+        *              "sortkey": { "name of sort criteria": "sortkey", ... }
+        *              "tags": [ "tag1", "tag2", ... ]
+        *      }
+        * @param $items Array
+        * @return boolean
+        */
+               
+       private function validateItems( array $items ) {
+               if ( count( $items ) > self::MAX_LIST_SIZE ) {
+                       return false;
+               }
+               $itemsSoFar = [];
+               foreach( $items as $item ) {
+                       // Fixme do we enforce uniqueness on title?
+                       if ( !property_exists( $item, 'title' )
+                               || !is_string( $item->title )
+                               || isset( $itemsSoFar[$item->title] )
+                       ) {
+                               return false;
+                       }
+                       $itemsSoFar[$item->title] = true;
+                       foreach( $item as $field => $value ) {
+                               switch( $field ) {
+                               case 'title':
+                               case 'notes':
+                                       if( !is_string( $value ) ) {
+                                               return false;
+                                       }
+                                       break;
+                               case 'link':
+                               case 'image':
+                                       if ( $value === false ) {
+                                               break;
+                                       }
+                                       if ( !is_string( $value ) ||
+                                               !Title::makeTitleSafe( NS_MAIN, 
$value )
+                                       ) {
+                                               return false;
+                                       }
+                                       break;
+                               case 'sortkey':
+                                       // FIXME: Validate it matches options.
+                                       if( !is_object( $sortkey ) ) {
+                                               return false;
+                                       }
+                                       foreach( $value as $keyname => $keytext 
) {
+                                               if( !is_string( $keytext ) ) {
+                                                       return false;
+                                               }
+                                       }
+                                       break;
+                               case 'tags':
+                                       if( !is_array( $value ) ) {
+                                               return false;
+                                       }
+                                       foreach( $value as $tag ) {
+                                               if( !is_string( $tag ) ) {
+                                                       return false;
+                                               }
+                                               if ( !Title::makeTitleSafe( 0, 
$tag ) ) {
+                                                       // Title is not really 
exactly
+                                                       // what we need. It 
bans some
+                                                       // things we don't care 
about,
+                                                       // like "..". But all 
in all,
+                                                       // its a good proxy for 
a sane
+                                                       // tag name.
+                                                       return false;
+                                               }
+                                       }
+                                       break;
+                               default:
+                                       return false;
+                               }
+                       }
+               }
+               return true;
+       }
+
+       /**
+        * Validate the options structure
+        *
+        * @param $options stdClass
+        * @return boolean
+        * @todo FIXME implement
+        */
+       private function validateOptions( stdClass $options ) {
+               return true;
+       }
+
+       /**
+        * Decode the JSON contents and populate protected variables.
+        */
+       protected function decode() {
+               if ( $this->decoded ) {
+                       return;
+               }
+               if ( !$this->isValid() ) {
+                       throw new Exception( "Can't decode invalid content" );
+               }
+               $data = $this->getData()->value;
+               $this->description = $data->description;
+               $this->options = $data->options;
+               $this->items = $data->items;
+               $this->decoded = true;
+       }
+
+       // Some placeholder getters; add the rest if there's ever any reason to
+
+       /**
+        * @return string
+        */
+       public function getDescription() {
+               $this->decode();
+               return $this->description;
+       }
+
+       /**
+        * Fill $output with information derived from the content.
+        * @param Title $title
+        * @param int $revId
+        * @param ParserOptions $options
+        * @param bool $generateHtml
+        * @param ParserOutput $output
+        */
+       protected function fillParserOutput( Title $title, $revId, 
ParserOptions $options,
+               $generateHtml, ParserOutput &$output
+       ) {
+               global $wgParser;
+               $this->decode();
+
+               $lang = $options->getTargetLanguage();
+               if ( !$lang ) {
+                       $lang = $title->getPageLanguage();
+               }
+               $text = $this->convertToWikitext( $lang, true );
+               $output = $wgParser->parse( $text, $title, $options, true, 
true, $revId );
+       }
+
+       /**
+        * Convert the JSON to wikitext.
+        *
+        * @param $lang Language The (content) language to render the page in.
+        * @param $includeDesc boolean Include the description
+        * @return string The wikitext
+        */
+       private function convertToWikitext( Language $lang, $includeDesc = 
false, $maxItems = false ) {
+               $this->decode();
+
+               $text = "__NOEDITSECTION__\n__NOTOC__";
+               if ( includeDesc ) {
+                       $text .= $this->getDescription() . "\n";
+               }
+               if ( count( $this->items ) === 0 ) {
+                       $text .= "<hr>\n" . wfMessage( 
'collaborationkit-listempty' )
+                               ->inLanguage( $lang )
+                               ->plain() . "\n";
+               }
+               $curItem = 0;
+               foreach( $this->items as $item ) {
+                       $curItem++;
+                       if ( $maxItems !== false && $maxItems < $curItem ) {
+                               break;
+                       }
+
+                       $titleForItem = null;
+                       if ( !isset( $item->link ) ) {
+                               $titleForItem = Title::newFromText( 
$item->title );
+                       } elseif ( $item->link !== false ) {
+                               $titleForItem = Title::newFromText( $item->link 
);
+                       }
+                       if ( $titleForItem ) {
+                               $text .= "==[[" . 
$titleForItem->getPrefixedDBkey() . "|"
+                                       . wfEscapeWikiText( $item->title ) . 
"]]==\n";
+                       } else {
+                               $text .= "==" . wfEscapeWikiText( $item->title 
) . "==\n";
+                       }
+
+                       $image = null;
+                       if ( !isset( $item->image ) && $titleForItem ) {
+                               if ( class_exists( 'PageImages' ) ) {
+                                       $image = PageImages::getPageImage( 
$titleForItem );
+                               }
+                       } elseif( is_string( $item->image ) ) {
+                               $imageTitle = Title::newFromText( $item->image, 
NS_FILE );
+                               if ( $imageTitle ) {
+                                       $image = wfFindFile( $imageTitle );
+                               }
+                       }
+
+                       if ( $image ) {
+                               $text .= '[[File:' . $image->getName() . 
"|left|100x100px|alt=]]\n";
+                       }
+
+                       if ( is_string( $item->notes ) ) {
+                               $text .= $item->notes . "\n";
+                       }
+
+                       if ( is_array( $item->tags ) && count( $item->tags ) ) {
+                               $text .= "\n<div class='toccolours' 
style='display:inline-block'>" .
+                                       wfMessage( 'collaborationkit-taglist')
+                                               ->inLanguage( $lang )
+                                               ->params(
+                                                       $lang->commaList(
+                                                               array_map( 
'wfEscapeWikiText', $item->tags )
+                                                       )
+                                               )->numParams( count( 
$item->tags ) )
+                                               ->plain() .
+                                       "</div>\n";
+                       }
+                       if ( $image ) {
+                               $text .= "<br style='clear:both'>\n";
+                       }
+               }
+               return $text;
+       }
+
+       public function convert( $toModel, $lossy = '' ) {
+               if ( $toModel === CONTENT_MODEL_WIKITEXT && $lossy === 'lossy' 
) {
+                       global $wgContLang;
+                       // using wgContLang is kind of icky. Maybe we should 
transclude
+                       // from mediawiki namespace, or give up on not 
splitting the
+                       // parser cache and just use {{int:... (?)
+                       $text = $this->convertToWikitext( $wgContLang, true );
+                       return ContentHandler::makeContent( $text, null, 
$toModel );
+               } elseif( $toModel === CONTENT_MODEL_JSON ) {
+                       return ContentHandler::makeContent( 
$this->getNativeData(), null, $toModel );
+               }
+               return parent::convert( $toModel, $lossy );
+       }
+
+       public function getWikitextForTransclusion() {
+               global $wgContLang;
+               // FIXME Unclear if we should really do this as a transclusion, 
or
+               // introduce a parser function. Too bad we don't have access to
+               // transclusion parameters from this interface w/o doing 
something
+               // insane:(
+               $text = $this->convertToWikitext( $wgContLang, false, 5 );
+               return $text;
+       }
+}
diff --git a/includes/content/CollaborationListContentHandler.php 
b/includes/content/CollaborationListContentHandler.php
new file mode 100644
index 0000000..654c882
--- /dev/null
+++ b/includes/content/CollaborationListContentHandler.php
@@ -0,0 +1,76 @@
+<?php
+
+// We extend TextContentHandler instead of JsonContentHandler since
+// we do not display this as Json code.
+class CollaborationListContentHandler extends TextContentHandler {
+
+       public function __construct( $modelId = 'CollaborationListContent' ) {
+               // FIXME, second argument should be [ CONTENT_FORMAT_JSON ],
+               // but I don't understand enough about content handler to figure
+               // out what that actually does...
+               parent::__construct( $modelId );
+       }
+
+       /**
+        * @param string $text
+        * @param string $format
+        * @return CollaborationListContent
+        * @throws MWContentSerializationException
+        */
+       public function unserializeContent( $text, $format = null ) {
+               $this->checkFormat( $format );
+               $content = new CollaborationListContent( $text );
+               if ( !$content->isValid() ) {
+                       throw new MWContentSerializationException( 'The 
collaborationlist content is invalid.' );
+               }
+               return $content;
+       }
+
+       /**
+        * @return CollaborationListContent
+        */
+       public function makeEmptyContent() {
+               $empty = <<<JSON
+                       {
+                               "items": [],
+                               "options": {},
+                               "description": ""
+                       }
+JSON;
+               return new CollaborationListContent( $empty );
+       }
+
+       /**
+        * @return string
+        */
+       protected function getContentClass() {
+               return 'CollaborationListContent';
+       }
+
+       /**
+        * @return string
+        */
+       /*protected function getDiffEngineClass() {
+               return 'CollaborationListDiffEngine';
+       }*/
+
+       /**
+        * FIXME is this really true?
+        * @return bool
+        */
+       public function isParserCacheSupported() {
+               return true;
+       }
+
+/**** This disables Special:ChangeContentModel
+       public function supportsDirectEditing() {
+               return false;
+       }
+*/
+
+       public function supportsDirectApiEditing() {
+               return true;
+       }
+}
+
+

-- 
To view, visit https://gerrit.wikimedia.org/r/290963
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I343a374ff00d40f84a3bdabdaee28b8f6d27c5a4
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/CollaborationKit
Gerrit-Branch: master
Gerrit-Owner: Brian Wolff <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to