<?php
/**
 * Implements Special:MultiUpload
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 * http://www.gnu.org/copyleft/gpl.html
 *
 * @file
 * @ingroup SpecialPage
 * @ingroup Upload
 */
if ( !defined( 'MEDIAWIKI' ) ) {
	die();
}

/* use the local, patched version of SpecialUpload.php for now */
global $wgVersion;
if ( version_compare( $wgVersion, '1.22', '<' ) ) {
	$wgAutoloadLocalClasses['SpecialUpload']
		= $wgAutoloadLocalClasses['UploadForm']
		= $wgAutoloadLocalClasses['UploadSourceField']
		= __DIR__ . '/SpecialUpload.1.21.3.php';
} else {
	$wgAutoloadLocalClasses['SpecialUpload']
		= $wgAutoloadLocalClasses['UploadForm']
		= $wgAutoloadLocalClasses['UploadSourceField']
		= __DIR__ . '/SpecialUpload.php';
}

/**
 * Special page for uploading multiple files in one submission.
 */
class SpecialMultiUpload extends SpecialUpload {

	function __construct( $request = null ) {
		SpecialPage::__construct( 'MultiUpload', 'upload' );
	}

	public $mFrom;   // which numbered rows to display, process
	public $mTo;

	public $mRows;

	/**
	 * Under which header this special page is listed in Special:SpecialPages
	 *
	 * @return string
	 */
	protected function getGroupName() {
		return 'media';
	}

	protected function handleRequestData() {
		$request = $this->getRequest();
		$this->checkToken( $request );
		$this->mFrom = 1; # $request->getVal( 'from', 1 );
		global $wgMultiUploadInitialNumberOfImportRows;
		$this->mTo = $request->getVal( 'wpLastRowIndex',
			$wgMultiUploadInitialNumberOfImportRows );
		# stick an invisible template row in front
		$this->mRows = array(
			$this->createRow( 'template' ),
		);
		$i = $this->mFrom;
		while ( $i <= $this->mTo ) {
			$this->mRows[] = $row = $this->createRow( $i );
			$row->handleRequestData();
			$i++;
		}
		$this->showUploadForm( $this->getUploadForm() );
	}

	protected function createRow( $i ) {
		$row = new UploadRow( $this, $i );
		$row->setContext( $this->getContext() );
		return $row;
	}

	/**
	 * Get an UploadForm instance with title and text properly set.
	 *
	 * @param $message String: HTML string to add to the form
	 * @param $sessionKey String: session key in case this is a stashed upload
	 * @param $hideIgnoreWarning true if warning's already been dealt with
	 * @return UploadForm
	 */
	protected function getUploadForm( $message = '', $sessionKey = '', $hideIgnoreWarning = false ) {
		# Initialize form
		$form = new MultiUploadForm( $this, $this->mRows, $this->mTo, $this->getContext() );
		$form->setTitle( $this->getTitle() );

		# Check the edit token.
		# Unlike Special:Upload, no fine distinctions about
		# whether they're uploading vs. cancelling, etc.
		if ( !$this->mTokenOk && $this->getRequest()->wasPosted() ) {
			$form->addPreText( $this->msg( 'session_fail_preview' )->parse() );
		}
		# Add the page-top text.
		$form->addPreText( $this->msg( 'multiupload-text' )->parse() );
		// @todo FIXME: add footer

		return $form;
	}

	public function getGlobalFormDescriptors() {
		return array();
	}
}

/**
 * Subclass of HTMLForm that provides the form section of Special:MultiUpload
 */
class MultiUploadForm extends UploadForm {
	protected $mPage;
	protected $mRows;
	protected $mLastIndex;

	public function __construct( $page, $rows, $lastIndex, IContextSource $context = null ) {
		$this->mPage = $page;
		$this->mRows = $rows;
		$this->mLastIndex = $lastIndex;
		parent::__construct( array(), $context );
		$this->mSourceIds = array();
		$this->mMessagePrefix = 'multiupload';
		$this->setSubmitText( wfMessage( 'multiupload-submit' )->parse() );
	}

	protected function constructData( array $options = array(), IContextSource $context = null ) {
	}

	protected function constructForm( IContextSource $context ) {
		$descriptor = $this->getGlobalFormDescriptors()
			+ $this->mPage->getGlobalFormDescriptors();
		foreach ( $this->mRows as $row ) {
			$rowdesc = $row->getFormDescriptors();
			$descriptor = $descriptor + $rowdesc;
		}
		HTMLForm::__construct( $descriptor, $context, 'upload' );
	}

	protected function getGlobalFormDescriptors() {
		return array(
			'LastRowIndex' => array(
				'type' => 'hidden',
				'id' => 'wpLastRowIndex',
				'default' => $this->mLastIndex,
			),
		);
	}

	public function getLegend( $key ) {
		$parts = explode( '-', $key );
		$msg = array_shift( $parts );
		return wfMessage( "{$this->mMessagePrefix}-$msg", $parts )->parse();
	}

	protected function addJsConfigVars( $out ) {
		parent::addJsConfigVars( $out );
		$jsConfig = array(
			'wpFirstRowIndex' => $this->mPage->mFrom,
			'wpLastRowIndex' => $this->mPage->mTo,
			'wgMultiUploadMaxPhpUploadSize' => min(
				wfShorthandToInteger( ini_get( 'upload_max_filesize' ) ),
				wfShorthandToInteger( ini_get( 'post_max_size' ) )
			),

		);
		foreach ( $this->mRows as $row ) {
			$jsConfig = $jsConfig + $row->jsConfigVars();
		}
		$out->addJsConfigVars( $jsConfig );
	}

	protected function addRLModules( $out ) {
		$out->addModules( array(
			'ext.multiupload.top',
			'ext.multiupload',
		) );
	}
}

/**
 * Hoping this gets merged into core, won't have to do it here
 */
if ( !class_exists( 'FauxWebRequestUpload' ) ) {
	/**
	 * A WebRequestUpload that can be faked.
	 */
	class FauxWebRequestUpload extends WebRequestUpload {
		/**
		 * Constructor. Should only be called by FauxRequest.
		 *
		 * @param $request WebRequest The associated request
		 * @param array $data Data in the same format that would be found
		 *          in the $_FILES array. If provided, will be used
		 *          instead of $_FILES[$key].
		 */
		public function __construct( $request, $data ) {
			$this->request = $request;
			$this->fileInfo = $data;
			$this->doesExist = true;
		}
	}
	/**
	 * allow DerivativeRequest to include fake uploaded files
	 */
	class DerivativeRequestWithFiles extends DerivativeRequest {
		/**
		 * @param string $key
		 * @return FauxWebRequestUpload|WebRequestUpload
		 */
		public function getUpload( $key ) {
			if ( array_key_exists( $key, $this->data ) ) {
				return new FauxWebRequestUpload( $this, $this->data[$key] );
			} else {
				return new WebRequestUpload( $this, $key );
			}
		}
	}
} else {
	/**
	 * If the feature is in MW core, just use it
	 */
	class DerivativeRequestWithFiles extends DerivativeRequest { }
}

class UploadRow extends SpecialUpload {
	public $mPage;
	public $mRowNumber;
	public $mRequest;
	public $mFormMessage;
	public $mSessionKey;
	public $mHideIgnoreWarning;
	public $mExtraButtons;

	/**
	 * Different constructor, let it know which row it is and
	 * the upload object it belongs to
	 */
	public function __construct( $page, $number ) {
		$this->mPage = $page;
		$this->setContext( $page->getContext() );
		$this->mRowNumber = $number;
		$this->mRequest = null;
		$this->mFormMessage = '';
		$this->mSessionKey = '';
		$this->mHideIgnoreWarning = '';
		$this->mExtraButtons = array();
	}

	/**
	 * UploadBase and various parent class methods expect certain
	 * form field names that don't have a row number appended.
	 * Here we create a fake request object that responds to those field names.
	 *
	 * @return DerivativeRequestWithFiles
	 */
	public function getRequest() {
		if ( !$this->mRequest ) {
			$webRequest = $this->mPage->getRequest();
			$i = $this->mRowNumber;
			$valuesKept = $valuesAltered = array();

			foreach ( $webRequest->getValues() + $_FILES as $key => $value ) {
				$matches = null;
				$prefixMatch = preg_match( '/^(.*?)(\d+)$/', $key, $matches );
				if ( $prefixMatch === false ) {
					// ERROR
				} elseif ( $prefixMatch == 0 ) {
					// key has no row number
					$valuesKept[$key] = $value;
				} elseif ( $matches[2] == $this->mRowNumber ) {
					// key has my row number
					$valuesAltered[$matches[1]] = $value;
				}	// else it has some other row number
			}

			$this->mRequest = new DerivativeRequestWithFiles(
				$webRequest,
				$valuesKept + $valuesAltered,
				$webRequest->wasPosted()
			);
		}

		return $this->mRequest;
	}

	protected function handleRequestData() {
		$request = $this->getRequest();
		$this->mUploadSuccessful = $request->getCheck( 'wpUploadSuccessful' );
		parent::handleRequestData();
	}

	/**
	 * We don't do our own form output - we give all output to the
	 * page object to aggregate into a single form
	 */
	protected function showUploadForm( $form ) {
		// it gets called
		// wfDebug(" *** SHOWUPLOADFORM SHOULD NOT BE CALLED! *** \n");
	}

	/**
	 * Unlike the superclass, don't actually create a form object when
	 * this is called, wait and do it when the page object is ready to
	 * assemble its full output form.
	 *
	 * @param $message String: HTML string to add to the form
	 * @param $sessionKey String: session key in case this is a stashed upload
	 * @param $hideIgnoreWarning Boolean: whether to hide "ignore warning" check box
	 * @return UploadForm
	 * todo lots of redundancy here
	 */
	public function getUploadForm( $message = '', $sessionKey = '', $hideIgnoreWarning = false ) {
	}

	public function showUploadError( $message ) {
		$this->mFormMessage .= $this->getUploadError( $message );
	}

	protected function showRecoverableUploadError( $message ) {
		$this->mSessionKey = $this->mUpload->stashSession();
		$this->mFormMessage .= $this->getRecoverableUploadError( $message );
	}

	protected function showUploadWarning( $warnings ) {
		$warningHtml = $this->getUploadWarning( $warnings );
		if ( $warningHtml === false ) {
			return false;
		}

		$this->mSessionKey = $this->mUpload->stashSession();
		$this->mFormMessage .= $warningHtml;
		$this->mHideIgnoreWarning = true;
		# Special:Upload changes the 'Upload' button to
		# 'Submit modified file description', and adds two
		# additional submit buttons. We add the additional
		# two as check boxes, and just leave the
		# 'Upload' button below all rows.
		$this->mExtraButtons = array(
			'UploadIgnoreWarning' => 'ignorewarning',
			'CancelUpload' => 'reuploaddesc',
		);

		return true;
	}

	/**
	 * This is apparently a pretty bad one.
	 * Special:Upload replaces the whole page with an error page
	 * when this happens. I'll just do it as an error message added
	 * to the form. But if it happens, you should probably start
	 * over clean.
	 */
	protected function showFileDeleteError() {
		$this->mFormMessage .= '<div class="error">'
			. $this->getOutput()->msg(
				'filenotfound', $this->mUpload->getTempPath() )
			. '</div>';
	}

	/**
	 * Suppress error message that file is empty, because this
	 * happens normally when you don't fill all the rows of the form.
	 */
	protected function processVerificationError( $details ) {
		if (
			$details['status'] === UploadBase::EMPTY_FILE &&
			$this->mDesiredDestName === ''
		)
		{
			return;
		}
		parent::processVerificationError( $details );
	}

	protected function createFormRow() {
		return new UploadFormRow(
			$this,
			$this->getFormOptions(
				$this->mSessionKey,
				$this->mHideIgnoreWarning
			),
			$this->getContext()
		);
	}

	public function getFormDescriptors() {
		# Initialize form
		$form = $this->createFormRow();

		$preText = '';

		if ( $this->mDesiredDestName ) {
			$preText .= $this->getViewDeletedLinks();
		}

		# Give a notice if the user is uploading a file that has been deleted or moved
		# Note that this is independent from the message 'filewasdeleted' that requires JS
		$desiredTitleObj = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName );

		$delNotice = ''; // empty by default
		if ( $desiredTitleObj instanceof Title && !$desiredTitleObj->exists() ) {
			LogEventsList::showLogExtract(
				$delNotice,
				array( 'delete', 'move' ),
				$desiredTitleObj,
				'',
				array(
					'lim' => 10,
					'conds' => array( "log_action != 'revision'" ),
					'showIfEmpty' => false,
					'msgKey' => array( 'upload-recreate-warning' )
				)
			);
		}
		$preText .= $delNotice;

		$preText .= $this->mFormMessage;

		return $form->descriptor( $preText, $this->mExtraButtons,
				$this->mUploadSuccessful );
	}

	protected function shouldProcessUpload() {
		return ( !$this->mUploadSuccessful &&
				$this->mPage->mTokenOk && !$this->mCancelUpload &&
				( $this->getRequest()->getVal( 'wpDestFile' ) &&
				$this->mUploadClicked ) );
	}

	protected function uploadSucceeded() {
		$this->mDesiredDestName = $this->mLocalFile->getTitle()->getDBkey();
	}

	/**
	 * @return array
	 */
	public function jsConfigVars() {
		return array(
			'wgMultiUploadAutoFill' . $this->mRowNumber =>
				( !$this->mForReUpload &&
				// if mDestFile was provided in the request,
				// don't overwrite it by autofilling
				$this->mDesiredDestName === '' ),
		);
	}
}

class UploadFormRow extends UploadForm {
	public $mRow;

	function __construct( $row, array $options = array(), IContextSource $context = null ) {
		$this->mRow = $row;
		$this->constructData( $options, $context );
		# HTMLForm::__construct( array(), $context, 'upload' );
		# $this->mSourceIds = array();
	}

	protected function twoColumnDescriptor( $text, $section ) {
		return array(
			'type' => 'info',
			'raw' => true,
			'rawrow' => true,
			'default' => '<tr><td colspan="2">' . $text . '</td></tr>',
			'section' => $section,
		);
	}

	protected function uploadedMessage() {
		$destTitle = Title::newFromText( $this->mDestFile, NS_FILE );
		return '<div class="multiupload-success-message">'
			. wfMessage( 'multiupload-uploadedto',
				Linker::linkKnown(
					$destTitle,
					$destTitle->getText()
				) )->text()
			. '</div>';
	}

	protected function uploadSucceededDescriptor( $i, $sectionLabel ) {
		return array(
			'UploadedMessage' . $i => $this->twoColumnDescriptor(
				$this->uploadedMessage(),
				$sectionLabel
			),
			'DestFile' . $i => array(
				'type' => 'hidden',
				'default' => $this->mDestFile,
				'section' => $sectionLabel
			),
			'UploadSuccessful' . $i => array(
				'type' => 'hidden',
				'default' => true,
				'section' => $sectionLabel
			),
		);
	}

	public function descriptor( $preText = '', $extraButtons = array(),
			$uploadSuccessful = false ) {
		$descriptor = array();
		$i = $this->mRow->mRowNumber;
		$sectionLabel = 'row-' . $i;

		if ( $uploadSuccessful ) {
			return $this->uploadSucceededDescriptor( $i, $sectionLabel );
		}

		$sectionDescriptors = $this->getSourceSection()
				+ $this->getDescriptionSection()
				+ $this->getOptionsSection();
		$header = '';

		foreach ( array( $preText, $this->mHeader ) + $this->mSectionHeaders as $head ) {
			if ( $head != '' ) {
				if ( $header != '' ) {
					$header .= "<br/>\n";
				}
				$header .= $head;
			}
		}

		$preTextSection = array();
		if ( $header != '' ) {
			$preTextSection['Message'] = $this->twoColumnDescriptor(
				$header, $sectionLabel );
		}

		# a couple markers for the JavaScript animations
		if ( isset( $sectionDescriptors['DestFile'] ) ) {
			$sectionDescriptors['DestFile']['cssclass'] = 'multiupload-first-to-collapse multiupload-width-exemplar';
		}

		foreach ( $preTextSection + $sectionDescriptors as $name => $field ) {
			if ( isset( $field['id'] ) ) {
				# put the IDs that Special:Upload uses into
				# the class attributes, without numbers appended,
				# so that JavaScript routines can find them that
				# way
				if ( isset( $field['cssclass'] ) ) {
					$field['cssclass'] .= ' ' . $field['id'];
				} else {
					$field['cssclass'] = $field['id'];
				}
				# add the row number to the actual ID, for use
				# as distinct form fields.
				$field['id'] = $field['id'] . $i;
			}
			if ( isset( $field['radio-name'] ) ) {
				$field['radio-name'] = $field['radio-name'] . $i;
			}
			if ( isset( $field['section'] ) ) {
				if ( isset( $field['cssclass'] ) ) {
					$field['cssclass'] .= ' ';
				} else {
					$field['cssclass'] = '';
				}
				$field['cssclass'] .= 'mw-htmlform-section-'
					. str_replace( '/', '-', $field['section'] );
			}
			$field['section'] = $sectionLabel;
			$descriptor["$name$i"] = $field;
		}

		foreach ( $extraButtons as $key => $msg ) {
			$descriptor[$key] = array(
				'type' => 'check',
				'id' => $key,
				'label-message' => $msg,
				'section' => $sectionLabel,
			);
		}

		return $descriptor;
	}
}
