CVE-2018-25089: Insufficient Information (NVD-CWE-noinfo)
value: A vulnerability was found in glb Meetup Tag Extension 0.1 on MediaWiki. It has been rated as problematic. This issue affects some unknown processing of the component Link Attribute Handler. The manipulation leads to use of web link to untrusted target with window.opener access. Upgrading to version 0.2 is able to address this issue. The identifier of the patch is 850c726d6bbfe0bf270801fbb92a30babea4155c. It is recommended to upgrade the affected component. The identifier VDB-238157 was assigned to this vulnerability.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
CVSS v3 base score: 5.3
Published: 2023-08-28T13:15Z
Repository: https://github.com/glb/mediawiki-tag-extension-meetup
Language: Markdown
Files changed: CONTRIBUTING.md, LICENSE, Meetup/Meetup.class.php, Meetup/extension.json, Meetup/i18n/en.json, Meetup/modules/ext.Meetup.base.css, README.md
Commit date: 2018-02-21 19:04:46 -0500
Commit message: Mitigate "reverse tabnabbing" vulnerability

"Reverse Tabnabbing" is an attack where a page linked from the target
page is able to rewrite that page; the vulnerability is opened by use
of the `target="_blank"` link attribute.

Removed `target="_blank"` and added `rel="noopener noreferer"` as well
as advised in the OWASP cheat sheet.

https://www.owasp.org/index.php/HTML5_Security_Cheat_Sheet#Tabnabbing
https://www.owasp.org/index.php/Reverse_Tabnabbing

Diff: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..70b1aad
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,95 @@
+# Contributing
+
+When contributing to this repository, please first discuss the change you wish to make via issue,
+email, or any other method with the owners of this repository before making a change.
+
+Please note we have a code of conduct, please follow it in all your interactions with the project.
+
+## Pull Request Process
+
+1. Review the [coding conventions](https://www.mediawiki.org/wiki/Manual:Coding_conventions),
+   [security checklist for developers](https://www.mediawiki.org/wiki/Security_checklist_for_developers),
+   and the [pre-commit checklist](https://www.mediawiki.org/wiki/Manual:Pre-commit_checklist).
+1. Ensure any install or build dependencies are removed before the end of the layer when doing a
+   build.
+1. Update the `README.md` with details of changes to the interface, this includes new environment
+   variables, exposed ports, useful file locations and container parameters.
+1. Increase the version numbers in any examples files and the `README.md` to the new version that this
+   Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
+1. You may merge the Pull Request in once you have the sign-off of two other developers, or if you
+   do not have permission to do that, you may request the second reviewer to merge it for you.
+
+## Code of Conduct
+
+### Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, gender identity and expression, level of experience,
+nationality, personal appearance, race, religion, or sexual identity and
+orientation.
+
+### Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+  advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+  address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+  professional setting
+
+### Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+### Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+### Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+### Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at [http://contributor-covenant.org/version/1/4][version]
+
+[homepage]: http://contributor-covenant.org
+[version]: http://contributor-covenant.org/version/1/4/
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..9d9c72a
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,7 @@
+Copyright 2017, Geoff Baskwill and others
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Meetup/Meetup.class.php b/Meetup/Meetup.class.php
new file mode 100644
index 0000000..e365286
--- /dev/null
+++ b/Meetup/Meetup.class.php
@@ -0,0 +1,260 @@
+<?php
+/**
+ * Simple custom tag extension to render information from the Meetup API.
+ *
+ * PHP version 5
+ *
+ * @ingroup Extensions
+ * @author  Geoff Baskwill <me@geoffbaskwill.ca>
+ * @version 0.1
+ * @link    https://github.com/glb/mediawiki-tag-extension-meetup
+ * @license https://opensource.org/licenses/MIT MIT
+ */
+
+/**
+ * Protect against register_globals vulnerabilities.
+ * This line must be present before any global variable is referenced.
+ */
+if( !defined( 'MEDIAWIKI' ) ) {
+	echo( "This is an extension to the MediaWiki package and cannot be run standalone.\n" );
+	die( -1 );
+}
+
+// Extension credits that will show up on Special:Version
+$wgExtensionCredits['parserhook'][] = array(
+	'path' => __FILE__,
+	'name' => 'Meetup',
+	'version' => '0.1',
+	'author' => 'Geoff Baskwill',
+	'url' => 'https://github.com/glb/mediawiki-tag-extension-meetup',
+	'descriptionmsg' => 'meetup-desc', // Message key in i18n file.
+	'description' => 'Adds <code>&lt;meetup&gt;</code> tag for including Meetup information in pages',
+);
+
+/**
+ * Simple custom tag extension to render information from the Meetup API.
+ *
+ * This class is based on the sample tag extension documented at
+ * https://www.mediawiki.org/wiki/Manual:Tag_extensions
+ *
+ * Special thanks to the Magic:NoCache hook for the special magic
+ * required to disable caching so that the page always uses fresh
+ * data from the API.
+ *
+ * https://phabricator.wikimedia.org/diffusion/EMNC/browse/master/MagicNoCache.hooks.php
+ *
+ * @ingroup Extensions
+ * @author  Geoff Baskwill <me@geoffbaskwill.ca>
+ * @link    https://github.com/glb/mediawiki-tag-extension-meetup
+ * @license https://opensource.org/licenses/MIT MIT
+ */
+class Meetup
+{
+	/**
+	 * Register our custom render callback with the parser.
+	 *
+	 * @param Parser $parser The Parser object that the function should
+	 *                       register its hook with.
+	 *
+	 * @return true
+	 */
+	public static function init( $parser )
+	{
+		// When the parser sees the <meetup> tag, it executes renderTag
+		$parser->setHook( 'meetup', array( 'Meetup', 'renderTag') );
+		return true;
+	}
+
+	/**
+	 * Render <meetup> tag.
+	 *
+	 * This function renders a custom <meetup group="{group ID}" /> tag,
+	 * reaching out to the meetup.com API to get details of upcoming and past
+	 * meetups.
+	 *
+	 * @param string  $input  The content of the tag (ignored as we don't render it).
+	 * @param array   $param  The array of parameters passed to the tag.
+	 * @param Parser  $parser The Parser -- needed to turn off caching
+	 * @param PPFrame $frame  ignored
+	 *
+	 * @global OutputPage $wgOut The OutputPage -- we need it to turn off
+	 *                           caching for this page.
+	 * @global array      $wgAction The action being performed -- we need it to
+	 *                              control whether we turn off caching.
+	 *
+	 * @return string content to be displayed
+	 */
+	public static function renderTag( $input, $param = array(), $parser = null, $frame = false) {
+		global $wgOut, $wgAction;
+
+		$wgOut->addModules( 'ext.meetup.base' );
+
+		if ( !in_array($wgAction, ['submit', 'edit']) ) {
+			$parser->disableCache();
+			$wgOut->enableClientCache(false);
+		}
+
+		// This isn't particularly great if the wiki is set up with a CSP that
+		// disallows inline CSS. A better solution would move the CSS to a
+		// resource.
+
+//		$ret = '<style>' .
+//		       '.meetup-description { font-weight: lighter; display: block; }' .
+//		       '.meetup-venue { font-style: italic; display: block; }' .
+//		       'li.event { margin-bottom: 1em; }' .
+//		       '</style>';
+
+		$ret .= Html::rawElement( 'h2', array(), wfMessage('meetup-header')->parse() );
+
+		$group = $param['group'];
+
+		if ( $group === false || $group == "" ||  $group == "{groupID}" ) {
+			$ret .= Html::rawElement( 'p', array(),
+				wfMessage( 'meetup-missing-group-info' )->parse()
+			);
+			return $ret;
+		}
+
+		// Ensure that the user-provided input (group ID) is encoded for the
+		// context it will be used in -- which is always a URL
+		$group = urlencode($group);
+
+		$ret .= Html::rawElement( 'a', array(
+				'rel' => 'nofollow noopener noreferrer',
+				'class' => 'external text meetup-badge',
+				'href' => wfMessage( 'meetup-group-ui-url', $group )->plain(),
+			), wfMessage( 'meetup-group-link-text')->parse() );
+
+		$ret .= Meetup::_getAndRenderEvents(
+			$group, 'upcoming', 5, 'false',
+			'meetup-upcoming-events-header',
+			'meetup-all-upcoming-events',
+			'meetup-no-events-scheduled'
+		);
+
+		$ret .= Meetup::_getAndRenderEvents(
+			$group, 'past', 5, 'true',
+			'meetup-past-events-header',
+			'meetup-all-past-events',
+			'meetup-no-past-events'
+		);
+
+		return $ret;
+	}
+
+	/**
+	 * Retrieve events from the meetup.com API and render them.
+	 *
+	 * This function retrieves the desired number of events from the API
+	 * and renders the event details in a list.
+	 *
+	 * @param string $group          The meetup.com group ID.
+	 * @param string $status         The type of events (past, upcoming, ...)
+	 *                               to retrieve and render.
+	 * @param int	 $count          The number of events to render.
+	 * @param string $desc           'true' to order events in descending order,
+	 *                               'false' otherwise.
+	 * @param string $header_message The message (see i18n/*.json) to render as
+	 *                               the header for this section.
+	 * @param string $link_message   The message (see i18n/*.json) to render as
+	 *                               the "see all" link for this section.
+	 * @param string $empty_message  The message (see i18n/*.json) to render if
+	 *                               there are no events for this section.
+	 *
+	 * @return string content to be displayed
+	 */
+	private static function _getAndRenderEvents( $group, $status, $count,
+		$desc, $header_message, $link_message, $empty_message
+	) {
+		$url = sprintf( '%s/%s/events' .
+				'?sign=true&photo-host=public&page=%d&status=%s&desc=%s',
+			wfMessage( 'meetup-api-base-url' )->plain(),
+			$group,
+			$count,
+			$status,
+			$desc
+		);
+
+		wfDebugLog( 'meetup', 'Retrieving events from API URL ' . $url );
+
+		$response = @file_get_contents( $url );
+		if ( $response === false ) {
+			wfDebugLog( 'meetup', 'Unable to retrieve events.' );
+			return "";
+		}
+
+		$events = json_decode( $response, true );
+
+		$ret .= Html::rawElement( 'h3', array(), wfMessage( $header_message )->parse() );
+
+		if (count($events) > 0 ) {
+			$ret .= '<ul>';
+			foreach ( $events as $event ) {
+				// First get rid of all the tags and then escape any remaining
+				// special characters in the description.
+				$description = htmlspecialchars(
+					preg_replace(
+						'/<.*?>/', '',
+						$event['description']
+					),
+					ENT_COMPAT | ENT_HTML401,
+					ini_get( "default_charset" ), false
+				);
+
+				// Then get rid of newlines; they make the formatting in the
+				// wiki go bad.
+				$description = preg_replace( '/\n+/', ' ', $description );
+
+				// Then cut to a max of 400 characters; if there are more then
+				// add a link to the event.
+				$description = preg_replace(
+					'/(.{400})(.+)/',
+					'\1 ' . Html::rawElement( 'a', array(
+						'rel' => 'nofollow noopener noreferrer',
+						'class' => 'external text',
+						'href' => $event['link'],
+					), wfMessage( 'meetup-read-more' )->parse() ),
+					$description
+				);
+
+				$ret .= Html::rawElement( 'li', array(
+						'class' => 'event',
+					), Html::rawElement( 'a', array(
+						'rel' => 'nofollow noopener noreferrer',
+						'class' => 'external text',
+						'href' => $event['link']
+					), sprintf( '%s (%s): %s',
+						date( 'M j, Y', $event['time']/1000 ),
+						htmlspecialchars( $event['local_time'] ),
+						htmlspecialchars( $event['name'] ) )
+					) . Html::rawElement( 'span', array(
+						'class' => 'meetup-venue',
+					), sprintf( '%s, %s · %s, %s',
+						htmlspecialchars( $event['venue']['name'] ),
+						htmlspecialchars( $event['venue']['address_1'] ),
+						htmlspecialchars( $event['venue']['city'] ),
+						htmlspecialchars( $event['venue']['state'] ) )
+					) . Html::rawElement( 'span', array(
+						'class' => 'meetup-description',
+					), $description ) // user-provided input has already been
+				);                       // escaped using htmlspecialchars
+			}
+			$ret .= '</ul>';
+
+			$ret .= Html::rawElement( 'a', array(
+					'rel' => 'nofollow noopener noreferrer',
+					'class' => 'external text',
+					'href' => wfMessage(
+						'meetup-events-ui-url', $group, $status
+					)->plain(),
+				), wfMessage( $link_message )->parse() );
+		} else {
+			$ret .= wfMessage( $empty_message )->parse();
+		}
+
+		return $ret;
+	}
+
+}
+
+?>
diff --git a/Meetup/extension.json b/Meetup/extension.json
new file mode 100644
index 0000000..da1394e
--- /dev/null
+++ b/Meetup/extension.json
@@ -0,0 +1,34 @@
+{
+  "name": "Meetup",
+  "author": "Geoff Baskwill",
+  "license-name": "MIT",
+  "url": "https://example.org",
+  "descriptionmsg": "meetup-desc",
+  "type": "parserhook",
+  "MessagesDirs": {
+    "Meetup": [
+      "i18n"
+    ]
+  },
+  "Hooks": {
+    "ParserFirstCallInit": [
+      "Meetup::init"
+    ]
+  },
+  "AutoloadClasses": {
+    "Meetup": "Meetup.class.php"
+  },
+  "ResourceModules": {
+    "ext.meetup.base": {
+      "styles": [
+        "modules/ext.Meetup.base.css"
+      ]
+    }
+  },
+  "ResourceFileModulePaths": {
+    "localBasePath": "",
+    "remoteExtPath": "Meetup"
+  },
+  "version": "0.2",
+  "manifest_version": 1
+}
diff --git a/Meetup/i18n/en.json b/Meetup/i18n/en.json
new file mode 100644
index 0000000..0938ca7
--- /dev/null
+++ b/Meetup/i18n/en.json
@@ -0,0 +1,21 @@
+{
+  "@metadata": {
+    "authors": []
+  },
+  "meetup-all-past-events": "See all past events on meetup.com",
+  "meetup-all-upcoming-events": "See all upcoming events on meetup.com",
+  "meetup-api-base-url": "https://api.meetup.com",
+  "meetup-desc": "Adds <code>&lt;meetup&gt;</code> tag for including Meetup information in pages",
+  "meetup-event-link-text": "See event on meetup.com",
+  "meetup-event-status": "{$1} / {$2} attending, {$3} {$4} on the waitlist.",
+  "meetup-events-ui-url": "https://www.meetup.com/$1/events/$2",
+  "meetup-group-link-text": "Visit our group on meetup.com",
+  "meetup-group-ui-url": "https://www.meetup.com/$1",
+  "meetup-header": "Chapter Meetings",
+  "meetup-missing-group-info": "The meetup group information is missing. Set the group attribute in the meetup tag.",
+  "meetup-no-events-scheduled": "No events are scheduled at the moment.",
+  "meetup-no-past-events": "No past events are available at the moment.",
+  "meetup-past-events-header": "Past events",
+  "meetup-read-more": "(read more)",
+  "meetup-upcoming-events-header": "Upcoming events"
+}
diff --git a/Meetup/modules/ext.Meetup.base.css b/Meetup/modules/ext.Meetup.base.css
new file mode 100644
index 0000000..58b6e2e
--- /dev/null
+++ b/Meetup/modules/ext.Meetup.base.css
@@ -0,0 +1,3 @@
+.meetup-description { font-weight: lighter; display: block; }
+.meetup-venue { font-style: italic; display: block; }
+li.event { margin-bottom: 1em; }
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8f5da7b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,109 @@
+# Meetup tag extension for mediawiki
+
+This [mediawiki extension](https://www.mediawiki.org/wiki/Manual:Extensions) adds a custom `<meetup group="..." />` tag that you can include in your wiki page to incorporate
+information about upcoming and past events for the group on meetup.com.
+
+## Getting Started
+
+These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See [Deployment](#Deployment) for notes on how to deploy the project on a live system.
+
+### Prerequisites
+
+You need to have a mediawiki installation in order to properly test any changes you make. Luckily
+there is an extremely-handy Docker container available to make the process of running mediawiki easier.
+
+### Installing
+
+#### Running the mediawiki install
+
+This is only required the first time you run:
+
+```shell
+docker run \
+  --name some-mediawiki \
+  -v $PWD/data:/var/www/data \
+  -p 8080:80 \
+  -d mediawiki
+```
+
+Open a browser to `http://localhost:8080` and walk through the mediawiki installer. At the end you
+will get a `LocalSettings.php` file that you will need to download and edit as shown in [Deployment](#Deployment).
+
+#### Running mediawiki with the extension
+
+After editing `LocalSettings.php`, you can run mediawiki with the extension using:
+
+```shell
+docker run \
+  --name some-mediawiki \
+  -v $PWD/data:/var/www/data \
+  -v $PWD/Meetup:/var/www/html/extensions/Meetup \
+  -v $PWD/LocalSettings.php:/var/www/html/LocalSettings.php \
+  -p 8080:80 \
+  -d mediawiki
+```
+
+If Docker complains that the port or name is already in use, you may have another instance already running. Try the following steps to kill and remove the existing container:
+
+```shell
+docker kill some-mediawiki
+docker rm some-mediawiki
+```
+
+## Deployment
+
+### Required steps
+
+See the [official mediawiki instructions for installing extensions](https://www.mediawiki.org/wiki/Manual:Extensions#Installing_an_extension), or:
+
+1. Create a new folder called `Meetup` in `$IP/extensions` on your mediawiki server.
+1. Copy the `Meetup` folder contents to `$IP/extensions/Meetup`.
+1. Add the following to your `LocalSettings.php` file:
+   ```php
+   wfLoadExtension( 'Meetup' );
+   ```
+
+### Optional steps
+
+1. Add the `meetup` debug log group to get separate logs for the extension:
+   ```php
+   $wgDebugLogGroups = array(
+       'meetup' => '/var/log/' . $IP . '/meetup-debug.log',
+   );
+   ```
+   Make sure to use an appropriate log location for your installation.
+
+## Using the extension
+
+Once the extension has been installed, wiki editors can insert the `meetup` custom tag in their pages like this:
+
+```text
+<meetup group="{groupID}" />
+```
+
+Replace `{groupID}` with the group ID of your Meetup group, which you can get from the URL of your Meetup group. For example, if your Meetup group's page is at `https://www.meetup.com/OWASP-Ottawa` then your group ID is `OWASP-Ottawa`.
+
+**Make sure to use `Edit source` rather than `Edit`, otherwise your page will show the tag instead of your Meetup content!**
+
+## Contributing
+
+Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details the process for submitting pull requests.
+
+## Versioning
+
+We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/glb/mediawiki-tag-extension-meetup/tags).
+
+## Authors
+
+See the list of [contributors](https://github.com/your/project/contributors) who participated in this project.
+
+## License
+
+This project is licensed under the MIT License - see [LICENSE](LICENSE) for details.
+
+## Acknowledgments
+
+* This is a pretty simple extension based heavily on the [sample](https://www.mediawiki.org/wiki/Manual:Tag_extensions).
+* Special thanks to the [Magic:NoCache](https://phabricator.wikimedia.org/diffusion/EMNC/browse/master/MagicNoCache.hooks.php) authors for pointing out how to do the special magic required to disable caching so that the page always displays fresh data from the API.
+* ... and also to [@PurpleBooth](https://www.github.com/PurpleBooth) for the template used to kick off this `README` and the `CONTRIBUTING` file as well.
+* This extension was written for the OWASP project for use in their [chapter wiki pages](https://www.owasp.org/index.php/OWASP_Chapter). Thanks to Tiffany Long for her detailed requirements statement and helpful review during the initial development stage.

Fixed code: # Contributing

When contributing to this repository, please first discuss the change you wish to make via issue,
email, or any other method with the owners of this repository before making a change.

Please note we have a code of conduct, please follow it in all your interactions with the project.

## Pull Request Process

1. Review the [coding conventions](https://www.mediawiki.org/wiki/Manual:Coding_conventions),
   [security checklist for developers](https://www.mediawiki.org/wiki/Security_checklist_for_developers),
   and the [pre-commit checklist](https://www.mediawiki.org/wiki/Manual:Pre-commit_checklist).
1. Ensure any install or build dependencies are removed before the end of the layer when doing a
   build.
1. Update the `README.md` with details of changes to the interface, this includes new environment
   variables, exposed ports, useful file locations and container parameters.
1. Increase the version numbers in any examples files and the `README.md` to the new version that this
   Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
1. You may merge the Pull Request in once you have the sign-off of two other developers, or if you
   do not have permission to do that, you may request the second reviewer to merge it for you.

## Code of Conduct

### Our Pledge

In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.

### Our Standards

Examples of behavior that contributes to creating a positive environment
include:

* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

* The use of sexualized language or imagery and unwelcome sexual attention or
  advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
  address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
  professional setting

### Our Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.

### Scope

This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.

### Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.

### Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]

[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
Copyright 2017, Geoff Baskwill and others

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
<?php
/**
 * Simple custom tag extension to render information from the Meetup API.
 *
 * PHP version 5
 *
 * @ingroup Extensions
 * @author  Geoff Baskwill <me@geoffbaskwill.ca>
 * @version 0.1
 * @link    https://github.com/glb/mediawiki-tag-extension-meetup
 * @license https://opensource.org/licenses/MIT MIT
 */

/**
 * Protect against register_globals vulnerabilities.
 * This line must be present before any global variable is referenced.
 */
if( !defined( 'MEDIAWIKI' ) ) {
	echo( "This is an extension to the MediaWiki package and cannot be run standalone.\n" );
	die( -1 );
}

// Extension credits that will show up on Special:Version
$wgExtensionCredits['parserhook'][] = array(
	'path' => __FILE__,
	'name' => 'Meetup',
	'version' => '0.1',
	'author' => 'Geoff Baskwill',
	'url' => 'https://github.com/glb/mediawiki-tag-extension-meetup',
	'descriptionmsg' => 'meetup-desc', // Message key in i18n file.
	'description' => 'Adds <code>&lt;meetup&gt;</code> tag for including Meetup information in pages',
);

/**
 * Simple custom tag extension to render information from the Meetup API.
 *
 * This class is based on the sample tag extension documented at
 * https://www.mediawiki.org/wiki/Manual:Tag_extensions
 *
 * Special thanks to the Magic:NoCache hook for the special magic
 * required to disable caching so that the page always uses fresh
 * data from the API.
 *
 * https://phabricator.wikimedia.org/diffusion/EMNC/browse/master/MagicNoCache.hooks.php
 *
 * @ingroup Extensions
 * @author  Geoff Baskwill <me@geoffbaskwill.ca>
 * @link    https://github.com/glb/mediawiki-tag-extension-meetup
 * @license https://opensource.org/licenses/MIT MIT
 */
class Meetup
{
	/**
	 * Register our custom render callback with the parser.
	 *
	 * @param Parser $parser The Parser object that the function should
	 *                       register its hook with.
	 *
	 * @return true
	 */
	public static function init( $parser )
	{
		// When the parser sees the <meetup> tag, it executes renderTag
		$parser->setHook( 'meetup', array( 'Meetup', 'renderTag') );
		return true;
	}

	/**
	 * Render <meetup> tag.
	 *
	 * This function renders a custom <meetup group="{group ID}" /> tag,
	 * reaching out to the meetup.com API to get details of upcoming and past
	 * meetups.
	 *
	 * @param string  $input  The content of the tag (ignored as we don't render it).
	 * @param array   $param  The array of parameters passed to the tag.
	 * @param Parser  $parser The Parser -- needed to turn off caching
	 * @param PPFrame $frame  ignored
	 *
	 * @global OutputPage $wgOut The OutputPage -- we need it to turn off
	 *                           caching for this page.
	 * @global array      $wgAction The action being performed -- we need it to
	 *                              control whether we turn off caching.
	 *
	 * @return string content to be displayed
	 */
	public static function renderTag( $input, $param = array(), $parser = null, $frame = false) {
		global $wgOut, $wgAction;

		$wgOut->addModules( 'ext.meetup.base' );

		if ( !in_array($wgAction, ['submit', 'edit']) ) {
			$parser->disableCache();
			$wgOut->enableClientCache(false);
		}

		// This isn't particularly great if the wiki is set up with a CSP that
		// disallows inline CSS. A better solution would move the CSS to a
		// resource.

//		$ret = '<style>' .
//		       '.meetup-description { font-weight: lighter; display: block; }' .
//		       '.meetup-venue { font-style: italic; display: block; }' .
//		       'li.event { margin-bottom: 1em; }' .
//		       '</style>';

		$ret .= Html::rawElement( 'h2', array(), wfMessage('meetup-header')->parse() );

		$group = $param['group'];

		if ( $group === false || $group == "" ||  $group == "{groupID}" ) {
			$ret .= Html::rawElement( 'p', array(),
				wfMessage( 'meetup-missing-group-info' )->parse()
			);
			return $ret;
		}

		// Ensure that the user-provided input (group ID) is encoded for the
		// context it will be used in -- which is always a URL
		$group = urlencode($group);

		$ret .= Html::rawElement( 'a', array(
				'rel' => 'nofollow noopener noreferrer',
				'class' => 'external text meetup-badge',
				'href' => wfMessage( 'meetup-group-ui-url', $group )->plain(),
			), wfMessage( 'meetup-group-link-text')->parse() );

		$ret .= Meetup::_getAndRenderEvents(
			$group, 'upcoming', 5, 'false',
			'meetup-upcoming-events-header',
			'meetup-all-upcoming-events',
			'meetup-no-events-scheduled'
		);

		$ret .= Meetup::_getAndRenderEvents(
			$group, 'past', 5, 'true',
			'meetup-past-events-header',
			'meetup-all-past-events',
			'meetup-no-past-events'
		);

		return $ret;
	}

	/**
	 * Retrieve events from the meetup.com API and render them.
	 *
	 * This function retrieves the desired number of events from the API
	 * and renders the event details in a list.
	 *
	 * @param string $group          The meetup.com group ID.
	 * @param string $status         The type of events (past, upcoming, ...)
	 *                               to retrieve and render.
	 * @param int	 $count          The number of events to render.
	 * @param string $desc           'true' to order events in descending order,
	 *                               'false' otherwise.
	 * @param string $header_message The message (see i18n/*.json) to render as
	 *                               the header for this section.
	 * @param string $link_message   The message (see i18n/*.json) to render as
	 *                               the "see all" link for this section.
	 * @param string $empty_message  The message (see i18n/*.json) to render if
	 *                               there are no events for this section.
	 *
	 * @return string content to be displayed
	 */
	private static function _getAndRenderEvents( $group, $status, $count,
		$desc, $header_message, $link_message, $empty_message
	) {
		$url = sprintf( '%s/%s/events' .
				'?sign=true&photo-host=public&page=%d&status=%s&desc=%s',
			wfMessage( 'meetup-api-base-url' )->plain(),
			$group,
			$count,
			$status,
			$desc
		);

		wfDebugLog( 'meetup', 'Retrieving events from API URL ' . $url );

		$response = @file_get_contents( $url );
		if ( $response === false ) {
			wfDebugLog( 'meetup', 'Unable to retrieve events.' );
			return "";
		}

		$events = json_decode( $response, true );

		$ret .= Html::rawElement( 'h3', array(), wfMessage( $header_message )->parse() );

		if (count($events) > 0 ) {
			$ret .= '<ul>';
			foreach ( $events as $event ) {
				// First get rid of all the tags and then escape any remaining
				// special characters in the description.
				$description = htmlspecialchars(
					preg_replace(
						'/<.*?>/', '',
						$event['description']
					),
					ENT_COMPAT | ENT_HTML401,
					ini_get( "default_charset" ), false
				);

				// Then get rid of newlines; they make the formatting in the
				// wiki go bad.
				$description = preg_replace( '/\n+/', ' ', $description );

				// Then cut to a max of 400 characters; if there are more then
				// add a link to the event.
				$description = preg_replace(
					'/(.{400})(.+)/',
					'\1 ' . Html::rawElement( 'a', array(
						'rel' => 'nofollow noopener noreferrer',
						'class' => 'external text',
						'href' => $event['link'],
					), wfMessage( 'meetup-read-more' )->parse() ),
					$description
				);

				$ret .= Html::rawElement( 'li', array(
						'class' => 'event',
					), Html::rawElement( 'a', array(
						'rel' => 'nofollow noopener noreferrer',
						'class' => 'external text',
						'href' => $event['link']
					), sprintf( '%s (%s): %s',
						date( 'M j, Y', $event['time']/1000 ),
						htmlspecialchars( $event['local_time'] ),
						htmlspecialchars( $event['name'] ) )
					) . Html::rawElement( 'span', array(
						'class' => 'meetup-venue',
					), sprintf( '%s, %s · %s, %s',
						htmlspecialchars( $event['venue']['name'] ),
						htmlspecialchars( $event['venue']['address_1'] ),
						htmlspecialchars( $event['venue']['city'] ),
						htmlspecialchars( $event['venue']['state'] ) )
					) . Html::rawElement( 'span', array(
						'class' => 'meetup-description',
					), $description ) // user-provided input has already been
				);                       // escaped using htmlspecialchars
			}
			$ret .= '</ul>';

			$ret .= Html::rawElement( 'a', array(
					'rel' => 'nofollow noopener noreferrer',
					'class' => 'external text',
					'href' => wfMessage(
						'meetup-events-ui-url', $group, $status
					)->plain(),
				), wfMessage( $link_message )->parse() );
		} else {
			$ret .= wfMessage( $empty_message )->parse();
		}

		return $ret;
	}

}

?>
{
  "name": "Meetup",
  "author": "Geoff Baskwill",
  "license-name": "MIT",
  "url": "https://example.org",
  "descriptionmsg": "meetup-desc",
  "type": "parserhook",
  "MessagesDirs": {
    "Meetup": [
      "i18n"
    ]
  },
  "Hooks": {
    "ParserFirstCallInit": [
      "Meetup::init"
    ]
  },
  "AutoloadClasses": {
    "Meetup": "Meetup.class.php"
  },
  "ResourceModules": {
    "ext.meetup.base": {
      "styles": [
        "modules/ext.Meetup.base.css"
      ]
    }
  },
  "ResourceFileModulePaths": {
    "localBasePath": "",
    "remoteExtPath": "Meetup"
  },
  "version": "0.2",
  "manifest_version": 1
}
{
  "@metadata": {
    "authors": []
  },
  "meetup-all-past-events": "See all past events on meetup.com",
  "meetup-all-upcoming-events": "See all upcoming events on meetup.com",
  "meetup-api-base-url": "https://api.meetup.com",
  "meetup-desc": "Adds <code>&lt;meetup&gt;</code> tag for including Meetup information in pages",
  "meetup-event-link-text": "See event on meetup.com",
  "meetup-event-status": "{$1} / {$2} attending, {$3} {$4} on the waitlist.",
  "meetup-events-ui-url": "https://www.meetup.com/$1/events/$2",
  "meetup-group-link-text": "Visit our group on meetup.com",
  "meetup-group-ui-url": "https://www.meetup.com/$1",
  "meetup-header": "Chapter Meetings",
  "meetup-missing-group-info": "The meetup group information is missing. Set the group attribute in the meetup tag.",
  "meetup-no-events-scheduled": "No events are scheduled at the moment.",
  "meetup-no-past-events": "No past events are available at the moment.",
  "meetup-past-events-header": "Past events",
  "meetup-read-more": "(read more)",
  "meetup-upcoming-events-header": "Upcoming events"
}
.meetup-description { font-weight: lighter; display: block; }
.meetup-venue { font-style: italic; display: block; }
li.event { margin-bottom: 1em; }
# Meetup tag extension for mediawiki

This [mediawiki extension](https://www.mediawiki.org/wiki/Manual:Extensions) adds a custom `<meetup group="..." />` tag that you can include in your wiki page to incorporate
information about upcoming and past events for the group on meetup.com.

## Getting Started

These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See [Deployment](#Deployment) for notes on how to deploy the project on a live system.

### Prerequisites

You need to have a mediawiki installation in order to properly test any changes you make. Luckily
there is an extremely-handy Docker container available to make the process of running mediawiki easier.

### Installing

#### Running the mediawiki install

This is only required the first time you run:

```shell
docker run \
  --name some-mediawiki \
  -v $PWD/data:/var/www/data \
  -p 8080:80 \
  -d mediawiki
```

Open a browser to `http://localhost:8080` and walk through the mediawiki installer. At the end you
will get a `LocalSettings.php` file that you will need to download and edit as shown in [Deployment](#Deployment).

#### Running mediawiki with the extension

After editing `LocalSettings.php`, you can run mediawiki with the extension using:

```shell
docker run \
  --name some-mediawiki \
  -v $PWD/data:/var/www/data \
  -v $PWD/Meetup:/var/www/html/extensions/Meetup \
  -v $PWD/LocalSettings.php:/var/www/html/LocalSettings.php \
  -p 8080:80 \
  -d mediawiki
```

If Docker complains that the port or name is already in use, you may have another instance already running. Try the following steps to kill and remove the existing container:

```shell
docker kill some-mediawiki
docker rm some-mediawiki
```

## Deployment

### Required steps

See the [official mediawiki instructions for installing extensions](https://www.mediawiki.org/wiki/Manual:Extensions#Installing_an_extension), or:

1. Create a new folder called `Meetup` in `$IP/extensions` on your mediawiki server.
1. Copy the `Meetup` folder contents to `$IP/extensions/Meetup`.
1. Add the following to your `LocalSettings.php` file:
   ```php
   wfLoadExtension( 'Meetup' );
   ```

### Optional steps

1. Add the `meetup` debug log group to get separate logs for the extension:
   ```php
   $wgDebugLogGroups = array(
       'meetup' => '/var/log/' . $IP . '/meetup-debug.log',
   );
   ```
   Make sure to use an appropriate log location for your installation.

## Using the extension

Once the extension has been installed, wiki editors can insert the `meetup` custom tag in their pages like this:

```text
<meetup group="{groupID}" />
```

Replace `{groupID}` with the group ID of your Meetup group, which you can get from the URL of your Meetup group. For example, if your Meetup group's page is at `https://www.meetup.com/OWASP-Ottawa` then your group ID is `OWASP-Ottawa`.

**Make sure to use `Edit source` rather than `Edit`, otherwise your page will show the tag instead of your Meetup content!**

## Contributing

Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details the process for submitting pull requests.

## Versioning

We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/glb/mediawiki-tag-extension-meetup/tags).

## Authors

See the list of [contributors](https://github.com/your/project/contributors) who participated in this project.

## License

This project is licensed under the MIT License - see [LICENSE](LICENSE) for details.

## Acknowledgments

* This is a pretty simple extension based heavily on the [sample](https://www.mediawiki.org/wiki/Manual:Tag_extensions).
* Special thanks to the [Magic:NoCache](https://phabricator.wikimedia.org/diffusion/EMNC/browse/master/MagicNoCache.hooks.php) authors for pointing out how to do the special magic required to disable caching so that the page always displays fresh data from the API.
* ... and also to [@PurpleBooth](https://www.github.com/PurpleBooth) for the template used to kick off this `README` and the `CONTRIBUTING` file as well.
* This extension was written for the OWASP project for use in their [chapter wiki pages](https://www.owasp.org/index.php/OWASP_Chapter). Thanks to Tiffany Long for her detailed requirements statement and helpful review during the initial development stage.