зеркало из https://github.com/mozilla/pjs.git
Updated addons stuff, including translation support
This commit is contained in:
Родитель
0e4089419e
Коммит
7c821e488d
|
@ -4,7 +4,7 @@ require_once('Archive/Zip.php');
|
|||
class AddonsController extends AppController
|
||||
{
|
||||
var $name = 'Addons';
|
||||
var $uses = array('Addon', 'Platform', 'Application', 'Appversion');
|
||||
var $uses = array('Addon', 'Platform', 'Application', 'Appversion', 'Tag');
|
||||
var $components = array('Amo', 'Rdf', 'Versioncompare');
|
||||
var $scaffold;
|
||||
/**
|
||||
|
@ -18,10 +18,23 @@ class AddonsController extends AppController
|
|||
* Add new or new version of an add-on
|
||||
* @param int $id
|
||||
*/
|
||||
function add($id = 0) {
|
||||
function add($id = '') {
|
||||
$this->layout = 'developers';
|
||||
$this->set('addonTypes', $this->Amo->addonTypes);
|
||||
|
||||
//Determine if an adding a new addon or new version
|
||||
if ($id != '' && $this->Amo->checkOwnership($id)) {
|
||||
$this->Addon->id = $id;
|
||||
$existing = $this->Addon->read();
|
||||
$this->set('existing', $existing);
|
||||
$newAddon = false;
|
||||
}
|
||||
else {
|
||||
$newAddon = true;
|
||||
}
|
||||
$this->set('id', $id);
|
||||
$this->set('newAddon', $newAddon);
|
||||
|
||||
//Step 2: Parse uploaded file and if correct, display addon-wide information form
|
||||
if (isset($this->data['Addon']['add_step1'])) {
|
||||
//Check for model validation first (in step 1, this is just addon type)
|
||||
|
@ -53,7 +66,7 @@ class AddonsController extends AppController
|
|||
//Check for file extenion match
|
||||
if (!in_array($fileExtension, $allowedExtensions)) {
|
||||
$this->Addon->invalidate('file');
|
||||
$this->set('fileError', 'Disallowed file extension ('.$fileExtension.')');
|
||||
$this->set('fileError', sprintf(_('Disallowed file extension (%s)'), $fileExtension));
|
||||
$this->render('add_step1');
|
||||
die();
|
||||
}
|
||||
|
@ -67,7 +80,7 @@ class AddonsController extends AppController
|
|||
}
|
||||
else {
|
||||
$this->Addon->invalidate('file');
|
||||
$this->set('fileError', 'Could not move file');
|
||||
$this->set('fileError', _('Could not move file'));
|
||||
$this->render('add_step1');
|
||||
die();
|
||||
}
|
||||
|
@ -85,18 +98,18 @@ class AddonsController extends AppController
|
|||
}
|
||||
else {
|
||||
$this->Addon->invalidate('file');
|
||||
$this->set('fileError', 'No install.rdf present');
|
||||
$this->set('fileError', _('No install.rdf present'));
|
||||
$this->render('add_step1');
|
||||
die();
|
||||
}
|
||||
|
||||
//Use Rdf Component to parse install.rdf
|
||||
$manifestData = $this->Rdf->parseInstallManifest($fileContents);
|
||||
pr($manifestData);
|
||||
|
||||
//If the result is a string, it is an error message
|
||||
if (!is_array($manifestData)) {
|
||||
$this->Addon->invalidate('file');
|
||||
$this->set('fileError', 'The following error occurred while parsing install.rdf: '.$manifestData);
|
||||
$this->set('fileError', sprintf(_('The following error occurred while parsing install.rdf: %s'), $manifestData));
|
||||
$this->render('add_step1');
|
||||
die();
|
||||
}
|
||||
|
@ -104,7 +117,7 @@ pr($manifestData);
|
|||
//Check if install.rdf has an updateURL
|
||||
if (isset($manifestData['updateURL'])) {
|
||||
$this->Addon->invalidate('file');
|
||||
$this->set('fileError', 'Add-ons cannot use an external updateURL. Please remove this from install.rdf and try again.');
|
||||
$this->set('fileError', _('Add-ons cannot use an external updateURL. Please remove this from install.rdf and try again.'));
|
||||
$this->render('add_step1');
|
||||
die();
|
||||
}
|
||||
|
@ -112,7 +125,7 @@ pr($manifestData);
|
|||
//Check the GUID
|
||||
if (!isset($manifestData['id']) || !preg_match('/^(\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}|[a-z0-9-\._]*\@[a-z0-9-\._]+)$/i', $manifestData['id'])) {
|
||||
$this->Addon->invalidate('file');
|
||||
$this->set('fileError', 'The ID of this add-on is invalid or not set.');
|
||||
$this->set('fileError', _('The ID of this add-on is invalid or not set.'));
|
||||
$this->render('add_step1');
|
||||
die();
|
||||
}
|
||||
|
@ -120,7 +133,7 @@ pr($manifestData);
|
|||
//Make sure version has no spaces
|
||||
if (!isset($manifestData['version']) || preg_match('/.*\s.*/', $manifestData['version'])) {
|
||||
$this->Addon->invalidate('file');
|
||||
$this->set('fileError', 'The version of this add-on is invalid or not set. Versions cannot contain spaces.');
|
||||
$this->set('fileError', _('The version of this add-on is invalid or not set. Versions cannot contain spaces.'));
|
||||
$this->render('add_step1');
|
||||
die();
|
||||
}
|
||||
|
@ -129,75 +142,115 @@ pr($manifestData);
|
|||
$addonNames = $manifestData['name'];
|
||||
$addonDesc = $manifestData['description'];
|
||||
|
||||
//If adding a new version to existing addon, check author
|
||||
$existing = $this->Addon->findAllByGuid($manifestData['id']);
|
||||
//In case user said it was a new add-on when it is actually an update
|
||||
if ($existing = $this->Addon->findAllByGuid($manifestData['id'])) {
|
||||
if ($newAddon === true) {
|
||||
$newAddon = false;
|
||||
$this->set('newAddon', $newAddon);
|
||||
}
|
||||
$existing = $existing[0];
|
||||
}
|
||||
else {
|
||||
if ($newAddon === false) {
|
||||
$newAddon = true;
|
||||
$this->set('newAddon', $newAddon);
|
||||
}
|
||||
}
|
||||
|
||||
//Initialize targetApp checking
|
||||
$noMozApps = true;
|
||||
$versionErrors = array();
|
||||
|
||||
if(count($manifestData['targetApplication']) > 0) {
|
||||
if (count($manifestData['targetApplication']) > 0) {
|
||||
//Iterate through each target app and find it in the DB
|
||||
foreach($manifestData['targetApplication'] as $appKey => $appVal) {
|
||||
if($matchingApp = $this->Application->find(array('guid' => $appKey), null, null, -1)) {
|
||||
foreach ($manifestData['targetApplication'] as $appKey => $appVal) {
|
||||
if ($matchingApp = $this->Application->find(array('guid' => $appKey), null, null, -1)) {
|
||||
$noMozApps = false;
|
||||
|
||||
//Check if the minVersion is valid
|
||||
if(!$matchingMinVers = $this->Appversion->find(array(
|
||||
'application_id' => $matchingApp['Application']['id'],
|
||||
'version' => $appVal['minVersion'],
|
||||
'public' => 1
|
||||
), null, null, -1)) {
|
||||
$versionErrors[] = $appVal['minVersion'].' is not a valid version for '.$matchingApp['Application']['name'];
|
||||
if (!$matchingMinVers = $this->Appversion->find(array(
|
||||
'application_id' => $matchingApp['Application']['id'],
|
||||
'version' => $appVal['minVersion'],
|
||||
'public' => 1
|
||||
), null, null, -1)) {
|
||||
$versionErrors[] = sprintf(_('%s is not a valid version for %s'), $appVal['minVersion'], $matchingApp['Application']['name']);
|
||||
}
|
||||
|
||||
//Check if the maxVersion is valid
|
||||
if(!$matchingMaxVers = $this->Appversion->find(array(
|
||||
'application_id' => $matchingApp['Application']['id'],
|
||||
'version' => $appVal['maxVersion'],
|
||||
'public' => 1
|
||||
), null, null, -1)) {
|
||||
$versionErrors[] = $appVal['maxVersion'].' is not a valid version for '.$matchingApp['Application']['name'];
|
||||
if (!$matchingMaxVers = $this->Appversion->find(array(
|
||||
'application_id' => $matchingApp['Application']['id'],
|
||||
'version' => $appVal['maxVersion'],
|
||||
'public' => 1
|
||||
), null, null, -1)) {
|
||||
$versionErrors[] = sprintf(_('%s is not a valid version for %s'), $appVal['maxVersion'], $matchingApp['Application']['name']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Must have at least one mozilla app
|
||||
if($noMozApps === true) {
|
||||
if ($noMozApps === true) {
|
||||
$this->Addon->invalidate('file');
|
||||
$this->set('fileError', 'You must have at least one valid Mozilla Target Application.');
|
||||
$this->set('fileError', _('You must have at least one valid Mozilla Target Application.'));
|
||||
$this->render('add_step1');
|
||||
die();
|
||||
}
|
||||
|
||||
//Max/min version errors
|
||||
if(count($versionErrors) > 0) {
|
||||
if (count($versionErrors) > 0) {
|
||||
$this->Addon->invalidate('file');
|
||||
$errorStr = implode($versionErrors, '<br />');
|
||||
$this->set('fileError', 'The following errors were found in install.rdf:'.$errorStr);
|
||||
$this->set('fileError', _('The following errors were found in install.rdf:').'<br />'.$errorStr);
|
||||
$this->render('add_step1');
|
||||
die();
|
||||
}
|
||||
|
||||
//Get Platforms list
|
||||
$platformQry = $this->Platform->findAll();
|
||||
foreach($platformQry as $k => $v) {
|
||||
$platforms[$platformQry[$k]['Platform']['id']] = $platformQry[$k]['Platform']['name'];
|
||||
}
|
||||
}
|
||||
//If it is a search plugin, read the .src file
|
||||
else {
|
||||
|
||||
}
|
||||
|
||||
$this->set('platforms', $platforms);
|
||||
//Get tags based on addontype
|
||||
$tagsQry = $this->Tag->findAll(array('addontype_id' => $this->data['Addon']['addontype_id']),
|
||||
null, null, null, null, -1);
|
||||
foreach ($tagsQry as $k => $v) {
|
||||
$tags[$v['Tag']['id']] = $v['Tag']['name'];
|
||||
}
|
||||
|
||||
$info['name'] = (!empty($existing['Addon']['name'])) ? $existing['Addon']['name'] : $manifestData['name']['en-US'];
|
||||
$info['description'] = (!empty($existing['Addon']['description'])) ? $existing['Addon']['description'] : $manifestData['description']['en-US'];
|
||||
$info['homepage'] = (!empty($existing['Addon']['homepage'])) ? $existing['Addon']['homepage'] : $manifestData['homepageURL'];
|
||||
$info['version'] = $manifestData['version'];
|
||||
$info['summary'] = $existing['Addon']['summary'];
|
||||
|
||||
if (count($existing['Tag']) > 0) {
|
||||
foreach ($existing['Tag'] as $tag) {
|
||||
$info['selectedTags'][$tag['id']] = $tag['name'];
|
||||
}
|
||||
}
|
||||
else {
|
||||
$info['selectedTags'] = array();
|
||||
}
|
||||
|
||||
$this->set('tags', $tags);
|
||||
$this->set('info', $info);
|
||||
$this->set('fileName', $fileName);
|
||||
$this->set('fileSize', $fileSize);
|
||||
$this->set('manifestData', $manifestData);
|
||||
$this->render('add_step2');
|
||||
}
|
||||
elseif (isset($this->data['Addon']['add_step2'])) {
|
||||
|
||||
$this->render('add_step25');
|
||||
}
|
||||
elseif (isset($this->data['Addon']['add_step25'])) {
|
||||
//Get Platforms list
|
||||
$platformQry = $this->Platform->findAll();
|
||||
foreach ($platformQry as $k => $v) {
|
||||
$platforms[$v['Platform']['id']] = $v['Platform']['name'];
|
||||
}
|
||||
|
||||
$this->set('platforms', $platforms);
|
||||
$this->render('add_step3');
|
||||
}
|
||||
elseif (isset($this->data['Addon']['add_step3'])) {
|
||||
|
@ -205,10 +258,6 @@ pr($manifestData);
|
|||
}
|
||||
//Step 1: Add-on type and file upload
|
||||
else {
|
||||
if ($id != 0) {
|
||||
//updating add-on
|
||||
}
|
||||
|
||||
$this->set('fileError', '');
|
||||
$this->render('add_step1');
|
||||
}
|
||||
|
@ -225,10 +274,10 @@ pr($manifestData);
|
|||
/**
|
||||
* Edit add-on
|
||||
* @param int $id
|
||||
*/
|
||||
*
|
||||
function edit($id) {
|
||||
|
||||
}
|
||||
}*/
|
||||
|
||||
/**
|
||||
* Edit a version
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
<?php
|
||||
class AmoComponent extends Object {
|
||||
var $addonTypes = array();
|
||||
|
||||
/**
|
||||
* Called automatically
|
||||
* @param object $controller
|
||||
*/
|
||||
function startup(&$controller) {
|
||||
$this->getAddonTypes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates addonTypes array from DB
|
||||
*/
|
||||
function getAddonTypes() {
|
||||
$addontype =& new Addontype();
|
||||
|
||||
$addonTypes = $addontype->findAll('', '', '', '', '', -1);
|
||||
foreach ($addonTypes as $k => $v) {
|
||||
$this->addonTypes[$addonTypes[$k]['Addontype']['id']] = $addonTypes[$k]['Addontype']['name'];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
<?php
|
||||
|
||||
class Addon extends AppModel
|
||||
{
|
||||
var $name = 'Addon';
|
||||
var $belongsTo = array('Addontype');
|
||||
var $hasMany = array('Version' =>
|
||||
array('className' => 'Version',
|
||||
'conditions' => '',
|
||||
'order' => '',
|
||||
'limit' => '',
|
||||
'foreignKey' => 'addon_id',
|
||||
'dependent' => true,
|
||||
'exclusive' => false,
|
||||
'finderSql' => ''
|
||||
),
|
||||
'Preview' =>
|
||||
array('className' => 'Preview',
|
||||
'conditions' => '',
|
||||
'order' => '',
|
||||
'limit' => '',
|
||||
'foreignKey' => 'addon_id',
|
||||
'dependent' => true,
|
||||
'exclusive' => false,
|
||||
'finderSql' => ''
|
||||
),
|
||||
'Feature' =>
|
||||
array('classname' => 'Feature',
|
||||
'conditions' => '',
|
||||
'order' => '',
|
||||
'limit' => '',
|
||||
'foreignKey' => 'addon_id',
|
||||
'dependent' => true,
|
||||
'exclusive' => false,
|
||||
'finderSql' => ''
|
||||
)
|
||||
);
|
||||
var $hasAndBelongsToMany = array('User' =>
|
||||
array('className' => 'User',
|
||||
'joinTable' => 'addons_users',
|
||||
'foreignKey' => 'addon_id',
|
||||
'associationForeignKey'=> 'user_id',
|
||||
'conditions' => '',
|
||||
'order' => '',
|
||||
'limit' => '',
|
||||
'unique' => false,
|
||||
'finderSql' => '',
|
||||
'deleteQuery'=> '',
|
||||
),
|
||||
'Tag' =>
|
||||
array('className' => 'Tag',
|
||||
'joinTable' => 'addons_tags',
|
||||
'foreignKey' => 'addon_id',
|
||||
'associationForeignKey'=> 'tag_id',
|
||||
'conditions' => '',
|
||||
'order' => '',
|
||||
'limit' => '',
|
||||
'unique' => false,
|
||||
'finderSql' => '',
|
||||
'deleteQuery'=> '',
|
||||
)
|
||||
|
||||
);
|
||||
var $validate = array(
|
||||
'guid' => VALID_NOT_EMPTY,
|
||||
'name' => VALID_NOT_EMPTY,
|
||||
'description' => VALID_NOT_EMPTY,
|
||||
'addontype_id' => VALID_NUMBER,
|
||||
'os' => VALID_NOT_EMPTY,
|
||||
'tags' => VALID_NOT_EMPTY
|
||||
);
|
||||
}
|
||||
?>
|
|
@ -1,34 +0,0 @@
|
|||
<?php
|
||||
|
||||
|
||||
|
||||
?>
|
||||
<?php
|
||||
|
||||
echo $html->formTag('/addons/add/', 'post', array('enctype'=>'multipart/form-data'));
|
||||
|
||||
echo $html->hidden('Addon/add_step1');
|
||||
|
||||
echo _('Add-on Type:');
|
||||
echo $html->selectTag('Addon/addontype_id', $addonTypes);
|
||||
echo $html->tagErrorMsg('Addon/addontype_id', _('Please select the type of add-on you are submitting.'));
|
||||
|
||||
echo '<br />';//temporary
|
||||
|
||||
|
||||
echo sprintf(_('Max upload size: %dMB'), ini_get('upload_max_filesize'));
|
||||
|
||||
echo '<br />';//temporary
|
||||
|
||||
echo _('Add-on File:');
|
||||
echo $html->file('Addon/file');
|
||||
echo $html->tagErrorMsg('Addon/file', sprintf(_('File error: %s'),$fileError));
|
||||
|
||||
echo '<br />';//temporary
|
||||
|
||||
|
||||
echo $html->submit();
|
||||
|
||||
|
||||
|
||||
?>
|
|
@ -1,41 +1,49 @@
|
|||
<?=$html->formTag('/addons/add/')?>
|
||||
<?=$html->hidden('Addon/add_step2')?>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td><?=$html->input('Addon/Name', array('value'=>$manifestData['name']['en-US']))?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Version</td>
|
||||
<td><?=$manifestData['version']?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>File</td>
|
||||
<td><?=$fileName.' ('.$fileSize.' KB)'?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Author(s)</td>
|
||||
<td><?=$html->input('Addon/Author')?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Platforms</td>
|
||||
<td><?=$html->selectTag('Addon/Platform', $platforms, null, null, null, false)?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan=2>Target Application(s)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Homepage</td>
|
||||
<td><?=$html->input('Addon/Homepage', array('value'=>$manifestData['homepageURL']))?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td><?=$html->textarea('Addon/Description', array('value' => $manifestData['description']['en-US'],
|
||||
<?php
|
||||
echo '<h1>'.(($newAddon === true) ? _('Submit New Add-on') : sprintf(_('Update %s'), $info['name'])).'</h1>';
|
||||
echo '<h2>'._('Step 2 :: Add-on Information').'</h2>';
|
||||
echo $html->formTag('/addons/add/'.$id);
|
||||
echo $html->hidden('Addon/add_step2');
|
||||
echo '<table>';
|
||||
echo '<tr>';
|
||||
echo '<td>'._('Name').'</td>';
|
||||
echo '<td>'.$html->input('Addon/Name', array('value'=>$info['name'])).$html->tagErrorMsg('Addon/Name', _('Please enter the name of your add-on.')).'</td>';
|
||||
echo '</tr>';
|
||||
echo '<tr>';
|
||||
echo '<td>'._('Version').'</td>';
|
||||
echo '<td>'.$info['version'].'</td>';
|
||||
echo '</tr>';
|
||||
echo '<tr>';
|
||||
echo '<td>'._('File').'</td>';
|
||||
echo '<td>'.sprintf(_('%s (%d KB)'), $fileName, $fileSize).'</td>';
|
||||
echo '</tr>';
|
||||
echo '<tr>';
|
||||
echo '<td>'._('Author E-mail').'</td>';
|
||||
echo '<td>'.$html->input('Addon/Author', array('size' => 40)).$html->link(_('Add Author'), '').$html->tagErrorMsg('Addon/Author', _('Please enter at least one author.')).'</td>';
|
||||
echo '</tr>';
|
||||
echo '<tr>';
|
||||
echo '<td>'._('Categories').'</td>';
|
||||
echo '<td>'.$html->selectTag('Addon/Tags', $tags, $info['selectedTags'], array('multiple' => 'true', 'size' => 10), null, false).$html->tagErrorMsg('Addon/Tags', _('Please select at least one category.')).'</td>';
|
||||
echo '</tr>';
|
||||
echo '<tr>';
|
||||
echo '<td>'._('Homepage').'</td>';
|
||||
echo '<td>'.$html->input('Addon/Homepage', array('size' => 40, 'value' => $info['homepage'])).'</td>';
|
||||
echo '</tr>';
|
||||
echo '<tr>';
|
||||
echo '<td>'._('Summary').'</td>';
|
||||
echo '<td>'.$html->input('Addon/Summary', array('size' => 40, 'maxlength' => 100, 'value' => $info['summary'])).$html->tagErrorMsg('Addon/Summary', _('Please enter a summary of your add-on.')).'</td>';
|
||||
echo '</tr>';
|
||||
echo '<tr>';
|
||||
echo '<td>'._('Description').'</td>';
|
||||
echo '<td>'.$html->textarea('Addon/Description', array('value' => $info['description'],
|
||||
'rows' => 3,
|
||||
'cols' => 55
|
||||
))?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan=2><?=$html->submit()?></td>
|
||||
</tr>
|
||||
</table>
|
||||
)).$html->tagErrorMsg('Addon/Description', _('Please enter a description of your add-on.')).'</td>';
|
||||
echo '</tr>';
|
||||
echo '<tr>';
|
||||
echo '<td colspan=2>'.$html->checkbox('Addon/ShowEULA').' '._('I would like to add an End User License Agreement and/or Privacy Policy').'</td>';
|
||||
echo '</tr>';
|
||||
echo '<tr>';
|
||||
echo '<td colspan=2>'.$html->submit(_('Next').' »').'</td>';
|
||||
echo '</tr>';
|
||||
echo '</table>';
|
||||
?>
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
echo '<h1>'.(($newAddon === true) ? _('Submit New Add-on') : sprintf(_('Update %s'), $existing['Addon']['name'])).'</h1>';
|
||||
echo '<h2>'._('Step 3 :: Version Information').'</h2>';
|
||||
echo $html->formTag('/addons/add/'.$id);
|
||||
echo $html->hidden('Addon/add_step3');
|
||||
echo '<table>';
|
||||
echo '<tr>';
|
||||
echo '<td>'._('Version').'</td>';
|
||||
echo '<td>'.$info['version'].'</td>';
|
||||
echo '</tr>';
|
||||
echo '<tr>';
|
||||
echo '<td>'._('Platforms').'</td>';
|
||||
echo '<td>'.$html->selectTag('Addon/Platform', $platforms, null, null, null, false).$html->tagErrorMsg('Addon/Platform', _('Please select the platforms your add-on suports.')).'</td>';
|
||||
echo '</tr>';
|
||||
echo '<tr>';
|
||||
echo '<td colspan=2>'._('Target Application(s)').'</td>';
|
||||
echo '</tr>';
|
||||
echo '<tr>';
|
||||
echo '<td>'._('Version Notes').'</td>';
|
||||
echo '<td>'.$html->textarea('Version/Releasenotes', array('rows' => 3, 'cols' => 55)).'</td>';
|
||||
echo '</tr>';
|
||||
echo '<tr>';
|
||||
echo '<td>'._('Notes to Reviewer').'</td>';
|
||||
echo '<td>'.$html->textarea('Version/Approvalnotes', array('rows' => 3, 'cols' => 55)).'</td>';
|
||||
echo '</tr>';
|
||||
echo '<tr>';
|
||||
echo '<td colspan=2>'.$html->submit(_('Next').' »').'</td>';
|
||||
echo '</tr>';
|
||||
echo '</table>';
|
||||
?>
|
Загрузка…
Ссылка в новой задаче