From 6aff818b72c9e2e94d3432288ec366ce7f16b86a Mon Sep 17 00:00:00 2001
From: Jennifer Richards <jennifer@staff.ietf.org>
Date: Wed, 9 Oct 2024 12:56:00 -0300
Subject: [PATCH] feat: remove deprecated /api/submit/ endpoint (#8017)

* feat: remove deprecated /api/submit endpoint

* chore: remove unused imports

* test: test api_submit_tombstone view
---
 ietf/api/urls.py                           |   2 +-
 ietf/submit/forms.py                       | 273 +--------------------
 ietf/submit/tests.py                       | 143 +----------
 ietf/submit/views.py                       | 107 +-------
 ietf/templates/api/index.html              |   2 +-
 ietf/templates/submit/api_submit_info.html |  57 +----
 6 files changed, 27 insertions(+), 557 deletions(-)

diff --git a/ietf/api/urls.py b/ietf/api/urls.py
index 396b3813d..48525dfda 100644
--- a/ietf/api/urls.py
+++ b/ietf/api/urls.py
@@ -59,7 +59,7 @@ urlpatterns = [
     # Email alias listing
     url(r'^person/email/$', api_views.active_email_list),
     # Draft submission API
-    url(r'^submit/?$', submit_views.api_submit),
+    url(r'^submit/?$', submit_views.api_submit_tombstone),
     # Draft upload API
     url(r'^submission/?$', submit_views.api_submission),
     # Draft submission state API
diff --git a/ietf/submit/forms.py b/ietf/submit/forms.py
index 4e5644b36..2781d3365 100644
--- a/ietf/submit/forms.py
+++ b/ietf/submit/forms.py
@@ -2,19 +2,16 @@
 # -*- coding: utf-8 -*-
 
 
-import io
 import os
 import re
 import datetime
 import email
 import sys
 import tempfile
-import xml2rfc
 from contextlib import ExitStack
 
 from email.utils import formataddr
 from typing import Tuple
-from unidecode import unidecode
 
 from django import forms
 from django.conf import settings
@@ -37,10 +34,8 @@ from ietf.submit.models import Submission, Preapproval
 from ietf.submit.utils import validate_submission_name, validate_submission_rev, validate_submission_document_date, remote_ip
 from ietf.submit.parsers.plain_parser import PlainParser
 from ietf.submit.parsers.xml_parser import XMLParser
-from ietf.utils import log
 from ietf.utils.draft import PlaintextDraft
 from ietf.utils.fields import ModelMultipleChoiceField
-from ietf.utils.text import normalize_text
 from ietf.utils.timezone import date_today
 from ietf.utils.xmldraft import InvalidXMLError, XMLDraft, XMLParseError
 
@@ -371,273 +366,6 @@ class SubmissionBaseUploadForm(forms.Form):
             return None
 
 
-class DeprecatedSubmissionBaseUploadForm(SubmissionBaseUploadForm):
-    def clean(self):
-        def format_messages(where, e, log):
-            out = log.write_out.getvalue().splitlines()
-            err = log.write_err.getvalue().splitlines()
-            m = str(e)
-            if m:
-                m = [ m ]
-            else:
-                import traceback
-                typ, val, tb = sys.exc_info()
-                m = traceback.format_exception(typ, val, tb)
-                m = [ l.replace('\n ', ':\n ') for l in m ]
-            msgs = [s for s in (["Error from xml2rfc (%s):" % (where,)] + m + out +  err) if s]
-            return msgs
-
-        if self.shutdown and not has_role(self.request.user, "Secretariat"):
-            raise forms.ValidationError(self.cutoff_warning)
-
-        for ext in self.formats:
-            f = self.cleaned_data.get(ext, None)
-            if not f:
-                continue
-            self.file_types.append('.%s' % ext)
-        if not ('.txt' in self.file_types or '.xml' in self.file_types):
-            if not self.errors:
-                raise forms.ValidationError('Unexpected submission file types; found %s, but %s is required' % (', '.join(self.file_types), ' or '.join(self.base_formats)))
-
-        #debug.show('self.cleaned_data["xml"]')
-        if self.cleaned_data.get('xml'):
-            #if not self.cleaned_data.get('txt'):
-            xml_file = self.cleaned_data.get('xml')
-            file_name = {}
-            xml2rfc.log.write_out = io.StringIO()   # open(os.devnull, "w")
-            xml2rfc.log.write_err = io.StringIO()   # open(os.devnull, "w")
-            tfn = None
-            with ExitStack() as stack:
-                @stack.callback
-                def cleanup():  # called when context exited, even in case of exception
-                    if tfn is not None:
-                        os.unlink(tfn)
-
-                # We need to write the xml file to disk in order to hand it
-                # over to the xml parser.  XXX FIXME: investigate updating
-                # xml2rfc to be able to work with file handles to in-memory
-                # files.
-                name, ext = os.path.splitext(os.path.basename(xml_file.name))
-                with tempfile.NamedTemporaryFile(prefix=name+'-',
-                                                 suffix='.xml',
-                                                 mode='wb+',
-                                                 delete=False) as tf:
-                    tfn = tf.name
-                    for chunk in xml_file.chunks():
-                        tf.write(chunk)
-
-                parser = xml2rfc.XmlRfcParser(str(tfn), quiet=True)
-                # --- Parse the xml ---
-                try:
-                    self.xmltree = parser.parse(remove_comments=False)
-                    # If we have v2, run it through v2v3. Keep track of the submitted version, though.
-                    self.xmlroot = self.xmltree.getroot()
-                    self.xml_version = self.xmlroot.get('version', '2')
-                    if self.xml_version == '2':
-                        v2v3 = xml2rfc.V2v3XmlWriter(self.xmltree)
-                        self.xmltree.tree = v2v3.convert2to3()
-                        self.xmlroot = self.xmltree.getroot()  # update to the new root
-
-                    draftname = self.xmlroot.attrib.get('docName')
-                    if draftname is None:
-                        self.add_error('xml', "No docName attribute found in the xml root element")
-                    name_error = validate_submission_name(draftname)
-                    if name_error:
-                        self.add_error('xml', name_error) # This is a critical and immediate failure - do not proceed with other validation.
-                    else:
-                        revmatch = re.search("-[0-9][0-9]$", draftname)
-                        if revmatch:
-                            self.revision = draftname[-2:]
-                            self.filename = draftname[:-3]
-                        else:
-                            self.revision = None
-                            self.filename = draftname
-                        self.title = self.xmlroot.findtext('front/title').strip()
-                        if type(self.title) is str:
-                            self.title = unidecode(self.title)
-                        self.title = normalize_text(self.title)
-                        self.abstract = (self.xmlroot.findtext('front/abstract') or '').strip()
-                        if type(self.abstract) is str:
-                            self.abstract = unidecode(self.abstract)
-                        author_info = self.xmlroot.findall('front/author')
-                        for author in author_info:
-                            info = {
-                                "name": author.attrib.get('fullname'),
-                                "email": author.findtext('address/email'),
-                                "affiliation": author.findtext('organization'),
-                            }
-                            elem = author.find('address/postal/country')
-                            if elem != None:
-                                ascii_country = elem.get('ascii', None)
-                                info['country'] = ascii_country if ascii_country else elem.text
-
-                            for item in info:
-                                if info[item]:
-                                    info[item] = info[item].strip()
-                            self.authors.append(info)
-
-                        # --- Prep the xml ---
-                        file_name['xml'] = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s%s' % (self.filename, self.revision, ext))
-                        try:
-                            prep = xml2rfc.PrepToolWriter(self.xmltree, quiet=True, liberal=True, keep_pis=[xml2rfc.V3_PI_TARGET])
-                            prep.options.accept_prepped = True
-                            self.xmltree.tree = prep.prep()
-                            if self.xmltree.tree == None:
-                                self.add_error('xml', "Error from xml2rfc (prep): %s" % prep.errors)
-                        except Exception as e:
-                                msgs = format_messages('prep', e, xml2rfc.log)
-                                self.add_error('xml', msgs)
-
-                        # --- Convert to txt ---
-                        if not ('txt' in self.cleaned_data and self.cleaned_data['txt']):
-                            file_name['txt'] = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s.txt' % (self.filename, self.revision))
-                            try:
-                                writer = xml2rfc.TextWriter(self.xmltree, quiet=True)
-                                writer.options.accept_prepped = True
-                                writer.write(file_name['txt'])
-                                log.log("In %s: xml2rfc %s generated %s from %s (version %s)" %
-                                        (   os.path.dirname(file_name['xml']),
-                                            xml2rfc.__version__,
-                                            os.path.basename(file_name['txt']),
-                                            os.path.basename(file_name['xml']),
-                                            self.xml_version))
-                            except Exception as e:
-                                msgs = format_messages('txt', e, xml2rfc.log)
-                                log.log('\n'.join(msgs))
-                                self.add_error('xml', msgs)
-
-                        # --- Convert to html ---
-                        try:
-                            file_name['html'] = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s.html' % (self.filename, self.revision))
-                            writer = xml2rfc.HtmlWriter(self.xmltree, quiet=True)
-                            writer.write(file_name['html'])
-                            self.file_types.append('.html')
-                            log.log("In %s: xml2rfc %s generated %s from %s (version %s)" %
-                                (   os.path.dirname(file_name['xml']),
-                                    xml2rfc.__version__,
-                                    os.path.basename(file_name['html']),
-                                    os.path.basename(file_name['xml']),
-                                    self.xml_version))
-                        except Exception as e:
-                            msgs = format_messages('html', e, xml2rfc.log)
-                            self.add_error('xml', msgs)
-
-                except Exception as e:
-                    try:
-                        msgs = format_messages('txt', e, xml2rfc.log)
-                        log.log('\n'.join(msgs))
-                        self.add_error('xml', msgs)
-                    except Exception:
-                        self.add_error('xml', "An exception occurred when trying to process the XML file: %s" % e)
-
-        # The following errors are likely noise if we have previous field
-        # errors:
-        if self.errors:
-            raise forms.ValidationError('')
-
-        if self.cleaned_data.get('txt'):
-            # try to parse it
-            txt_file = self.cleaned_data['txt']
-            txt_file.seek(0)
-            bytes = txt_file.read()
-            txt_file.seek(0)
-            try:
-                text = bytes.decode(PlainParser.encoding)
-                self.parsed_draft = PlaintextDraft(text, txt_file.name)
-                if self.filename == None:
-                    self.filename = self.parsed_draft.filename
-                elif self.filename != self.parsed_draft.filename:
-                    self.add_error('txt', "Inconsistent name information: xml:%s, txt:%s" % (self.filename, self.parsed_draft.filename))
-                if self.revision == None:
-                    self.revision = self.parsed_draft.revision
-                elif self.revision != self.parsed_draft.revision:
-                    self.add_error('txt', "Inconsistent revision information: xml:%s, txt:%s" % (self.revision, self.parsed_draft.revision))
-                if self.title == None:
-                    self.title = self.parsed_draft.get_title()
-                elif self.title != self.parsed_draft.get_title():
-                    self.add_error('txt', "Inconsistent title information: xml:%s, txt:%s" % (self.title, self.parsed_draft.get_title()))
-            except (UnicodeDecodeError, LookupError) as e:
-                self.add_error('txt', 'Failed decoding the uploaded file: "%s"' % str(e))
-
-        rev_error = validate_submission_rev(self.filename, self.revision)
-        if rev_error:
-            raise forms.ValidationError(rev_error)
-
-        # The following errors are likely noise if we have previous field
-        # errors:
-        if self.errors:
-            raise forms.ValidationError('')
-
-        if not self.filename:
-            raise forms.ValidationError("Could not extract a valid Internet-Draft name from the upload.  "
-                "To fix this in a text upload, please make sure that the full Internet-Draft name including "
-                "revision number appears centered on its own line below the document title on the "
-                "first page.  In an xml upload, please make sure that the top-level <rfc/> "
-                "element has a docName attribute which provides the full Internet-Draft name including "
-                "revision number.")
-
-        if not self.revision:
-            raise forms.ValidationError("Could not extract a valid Internet-Draft revision from the upload.  "
-                "To fix this in a text upload, please make sure that the full Internet-Draft name including "
-                "revision number appears centered on its own line below the document title on the "
-                "first page.  In an xml upload, please make sure that the top-level <rfc/> "
-                "element has a docName attribute which provides the full Internet-Draft name including "
-                "revision number.")
-
-        if not self.title:
-            raise forms.ValidationError("Could not extract a valid title from the upload")
-
-        if self.cleaned_data.get('txt') or self.cleaned_data.get('xml'):
-            # check group
-            self.group = self.deduce_group(self.filename)
-
-            # check existing
-            existing = Submission.objects.filter(name=self.filename, rev=self.revision).exclude(state__in=("posted", "cancel", "waiting-for-draft"))
-            if existing:
-                raise forms.ValidationError(mark_safe('A submission with same name and revision is currently being processed. <a href="%s">Check the status here.</a>' % urlreverse("ietf.submit.views.submission_status", kwargs={ 'submission_id': existing[0].pk })))
-
-            # cut-off
-            if self.revision == '00' and self.in_first_cut_off:
-                raise forms.ValidationError(mark_safe(self.cutoff_warning))
-
-            # check thresholds
-            today = date_today()
-
-            self.check_submissions_thresholds(
-                "for the Internet-Draft %s" % self.filename,
-                dict(name=self.filename, rev=self.revision, submission_date=today),
-                settings.IDSUBMIT_MAX_DAILY_SAME_DRAFT_NAME, settings.IDSUBMIT_MAX_DAILY_SAME_DRAFT_NAME_SIZE,
-            )
-            self.check_submissions_thresholds(
-                "for the same submitter",
-                dict(remote_ip=self.remote_ip, submission_date=today),
-                settings.IDSUBMIT_MAX_DAILY_SAME_SUBMITTER, settings.IDSUBMIT_MAX_DAILY_SAME_SUBMITTER_SIZE,
-            )
-            if self.group:
-                self.check_submissions_thresholds(
-                    "for the group \"%s\"" % (self.group.acronym),
-                    dict(group=self.group, submission_date=today),
-                    settings.IDSUBMIT_MAX_DAILY_SAME_GROUP, settings.IDSUBMIT_MAX_DAILY_SAME_GROUP_SIZE,
-                )
-            self.check_submissions_thresholds(
-                "across all submitters",
-                dict(submission_date=today),
-                settings.IDSUBMIT_MAX_DAILY_SUBMISSIONS, settings.IDSUBMIT_MAX_DAILY_SUBMISSIONS_SIZE,
-            )
-
-        return super().clean()
-
-
-class DeprecatedSubmissionAutoUploadForm(DeprecatedSubmissionBaseUploadForm):
-    """Full-service upload form, replaced by the asynchronous version"""
-    user = forms.EmailField(required=True)
-
-    def __init__(self, request, *args, **kwargs):
-        super(DeprecatedSubmissionAutoUploadForm, self).__init__(request, *args, **kwargs)
-        self.formats = ['xml', ]
-        self.base_formats = ['xml', ]
-
-
 class SubmissionManualUploadForm(SubmissionBaseUploadForm):
     txt = forms.FileField(label='.txt format', required=False)
     formats = SubmissionBaseUploadForm.formats + ('txt',)
@@ -676,6 +404,7 @@ class SubmissionManualUploadForm(SubmissionBaseUploadForm):
                 )
         return txt_file
 
+
 class SubmissionAutoUploadForm(SubmissionBaseUploadForm):
     user = forms.EmailField(required=True)
     replaces = forms.CharField(required=False, max_length=1000, strip=True)
diff --git a/ietf/submit/tests.py b/ietf/submit/tests.py
index b48168f8a..ed28c7ef0 100644
--- a/ietf/submit/tests.py
+++ b/ietf/submit/tests.py
@@ -44,7 +44,7 @@ from ietf.meeting.models import Meeting
 from ietf.meeting.factories import MeetingFactory
 from ietf.name.models import DraftSubmissionStateName, FormalLanguageName
 from ietf.person.models import Person
-from ietf.person.factories import UserFactory, PersonFactory, EmailFactory
+from ietf.person.factories import UserFactory, PersonFactory
 from ietf.submit.factories import SubmissionFactory, SubmissionExtResourceFactory
 from ietf.submit.forms import SubmissionBaseUploadForm, SubmissionAutoUploadForm
 from ietf.submit.models import Submission, Preapproval, SubmissionExtResource
@@ -2345,6 +2345,12 @@ class ApiSubmissionTests(BaseSubmitTestCase):
         super().setUp()
         MeetingFactory(type_id='ietf', date=date_today()+datetime.timedelta(days=60))
 
+    def test_api_submit_tombstone(self):
+        """Tombstone for obsolete API endpoint should return 410 Gone"""
+        url = urlreverse("ietf.submit.views.api_submit_tombstone")
+        self.assertEqual(self.client.get(url).status_code, 410)
+        self.assertEqual(self.client.post(url).status_code, 410)
+
     def test_upload_draft(self):
         """api_submission accepts a submission and queues it for processing"""
         url = urlreverse('ietf.submit.views.api_submission')
@@ -3191,141 +3197,6 @@ class AsyncSubmissionTests(BaseSubmitTestCase):
             self.assertEqual(subm.state_id, "cancel")
             self.assertEqual(subm.submissionevent_set.count(), 2)
 
-
-class ApiSubmitTests(BaseSubmitTestCase):
-    def setUp(self):
-        super().setUp()
-        # break early in case of missing configuration
-        self.assertTrue(os.path.exists(settings.IDSUBMIT_IDNITS_BINARY))
-        MeetingFactory(type_id='ietf', date=date_today()+datetime.timedelta(days=60))
-
-    def do_post_submission(self, rev, author=None, name=None, group=None, email=None, title=None, year=None):
-        url = urlreverse('ietf.submit.views.api_submit')
-        if author is None:
-            author = PersonFactory()
-        if name is None:
-            slug = re.sub('[^a-z0-9-]+', '', author.ascii_parts()[3].lower())
-            name = 'draft-%s-foo' % slug
-        if email is None:
-            email = author.user.username
-        # submit
-        data = {}
-        data['xml'], author = submission_file(f'{name}-{rev}', f'{name}-{rev}.xml', group, "test_submission.xml", author=author, email=email, title=title, year=year)
-        data['user'] = email
-        r = self.client.post(url, data)
-        return r, author, name
-
-    def test_api_submit_info(self):
-        url = urlreverse('ietf.submit.views.api_submit')
-        r = self.client.get(url)
-        expected = "A simplified Internet-Draft submission interface, intended for automation"
-        self.assertContains(r, expected, status_code=200)
-
-    def test_api_submit_bad_method(self):
-        url = urlreverse('ietf.submit.views.api_submit')
-        r = self.client.put(url)
-        self.assertEqual(r.status_code, 405)
-
-    def test_api_submit_ok(self):
-        r, author, name = self.do_post_submission('00')
-        expected = "Upload of %s OK, confirmation requests sent to:\n  %s" % (name, author.formatted_email().replace('\n',''))
-        self.assertContains(r, expected, status_code=200)
-
-    def test_api_submit_secondary_email_active(self):
-        person = PersonFactory()
-        email = EmailFactory(person=person)
-        r, author, name = self.do_post_submission('00', author=person, email=email.address)
-        for expected in [
-                "Upload of %s OK, confirmation requests sent to:" % (name, ),
-                author.formatted_email().replace('\n',''),
-            ]:
-            self.assertContains(r, expected, status_code=200)
-
-    def test_api_submit_secondary_email_inactive(self):
-        person = PersonFactory()
-        prim = person.email()
-        prim.primary = True
-        prim.save()
-        email = EmailFactory(person=person, active=False)
-        r, author, name = self.do_post_submission('00', author=person, email=email.address)
-        expected = "No such user: %s" % email.address
-        self.assertContains(r, expected, status_code=400)
-
-    def test_api_submit_no_user(self):
-        email='nonexistant.user@example.org'
-        r, author, name = self.do_post_submission('00', email=email)
-        expected = "No such user: %s" % email
-        self.assertContains(r, expected, status_code=400)
-
-    def test_api_submit_no_person(self):
-        user = UserFactory()
-        email = user.username
-        r, author, name = self.do_post_submission('00', email=email)
-        expected = "No person with username %s" % email
-        self.assertContains(r, expected, status_code=400)
-
-    def test_api_submit_wrong_revision(self):
-        r, author, name = self.do_post_submission('01')
-        expected = "Invalid revision (revision 00 is expected)"
-        self.assertContains(r, expected, status_code=400)
-
-    def test_api_submit_update_existing_submissiondocevent_rev(self):
-        draft, _ = create_draft_submission_with_rev_mismatch(rev='01')
-        r, _, __ = self.do_post_submission(rev='01', name=draft.name)
-        expected = "Submission failed"
-        self.assertContains(r, expected, status_code=409)
-
-    def test_api_submit_update_later_submissiondocevent_rev(self):
-        draft, _ = create_draft_submission_with_rev_mismatch(rev='02')
-        r, _, __ = self.do_post_submission(rev='01', name=draft.name)
-        expected = "Submission failed"
-        self.assertContains(r, expected, status_code=409)
-
-    def test_api_submit_pending_submission(self):
-        r, author, name = self.do_post_submission('00')
-        expected = "Upload of"
-        self.assertContains(r, expected, status_code=200)
-        r, author, name = self.do_post_submission('00', author=author, name=name)
-        expected = "A submission with same name and revision is currently being processed"
-        self.assertContains(r, expected, status_code=400)
-
-    def test_api_submit_no_title(self):
-        r, author, name = self.do_post_submission('00', title=" ")
-        expected = "Could not extract a valid title from the upload"
-        self.assertContains(r, expected, status_code=400)
-
-    def test_api_submit_failed_idnits(self):
-        # `year` on the next line must be leap year or this test will fail every Feb 29
-        r, author, name = self.do_post_submission('00', year="2012")
-        expected = "Document date must be within 3 days of submission date"
-        self.assertContains(r, expected, status_code=400)
-
-    def test_api_submit_keeps_extresources(self):
-        """API submit should not disturb doc external resources
-        
-        Tests that the submission inherits the existing doc's docextresource_set.
-        Relies on separate testing that Submission external_resources will be
-        handled appropriately.
-        """
-        draft = WgDraftFactory()
-
-        # add an external resource
-        self.assertEqual(draft.docextresource_set.count(), 0)
-        extres = draft.docextresource_set.create(
-            name_id='faq',
-            display_name='this is a display name',
-            value='https://example.com/faq-for-test.html',
-        )
-        
-        r, _, __ = self.do_post_submission('01', name=draft.name)
-        self.assertEqual(r.status_code, 200)
-        # draft = Document.objects.get(pk=draft.pk)  # update the draft
-        sub = Submission.objects.get(name=draft.name)
-        self.assertEqual(
-            [str(r) for r in sub.external_resources.all()],
-            [str(extres)],
-        )
-
         
 class RefsTests(BaseSubmitTestCase):
 
diff --git a/ietf/submit/views.py b/ietf/submit/views.py
index 6f23ba49d..3f745741e 100644
--- a/ietf/submit/views.py
+++ b/ietf/submit/views.py
@@ -1,7 +1,5 @@
 # Copyright The IETF Trust 2011-2020, All Rights Reserved
 # -*- coding: utf-8 -*-
-
-
 import re
 import datetime
 
@@ -29,17 +27,15 @@ from ietf.ietfauth.utils import has_role, role_required
 from ietf.mailtrigger.utils import gather_address_lists
 from ietf.person.models import Email
 from ietf.submit.forms import (SubmissionAutoUploadForm, AuthorForm, SubmitterForm, EditSubmissionForm,
-                               PreapprovalForm, ReplacesForm,
-                               DeprecatedSubmissionAutoUploadForm, SubmissionManualUploadForm)
+                               PreapprovalForm, ReplacesForm, SubmissionManualUploadForm)
 from ietf.submit.mail import send_full_url, send_manual_post_request
 from ietf.submit.models import (Submission, Preapproval, SubmissionExtResource,
     DraftSubmissionStateName )
 from ietf.submit.tasks import process_uploaded_submission_task, process_and_accept_uploaded_submission_task, poke
 from ietf.submit.utils import ( approvable_submissions_for_user, preapprovals_for_user,
     recently_approved_by_user, validate_submission, create_submission_event, docevent_from_submission,
-    post_submission, cancel_submission, rename_submission_files, remove_submission_files, get_draft_meta,
-    get_submission, fill_in_submission, apply_checkers, save_files, clear_existing_files,
-    check_submission_revision_consistency, accept_submission, accept_submission_requires_group_approval,
+    post_submission, cancel_submission, rename_submission_files, remove_submission_files,
+    get_submission, save_files, clear_existing_files, accept_submission, accept_submission_requires_group_approval,
     accept_submission_requires_prev_auth_approval, update_submission_external_resources)
 from ietf.stats.utils import clean_country_name
 from ietf.utils.accesstoken import generate_access_token
@@ -187,97 +183,14 @@ def api_submission_status(request, submission_id):
 
 
 @csrf_exempt
-def api_submit(request):
-    "Automated submission entrypoint"
-    submission = None
-    def err(code, text):
-        return HttpResponse(text, status=code, content_type='text/plain')
+def api_submit_tombstone(request):
+    """Tombstone for removed automated submission entrypoint"""
+    return render(
+        request, 
+        'submit/api_submit_info.html',
+        status=410,  # Gone
+    )
 
-    if request.method == 'GET':
-        return render(request, 'submit/api_submit_info.html')
-    elif request.method == 'POST':
-        exception = None
-        try:
-            form = DeprecatedSubmissionAutoUploadForm(request, data=request.POST, files=request.FILES)
-            if form.is_valid():
-                log('got valid submission form for %s' % form.filename)
-                username = form.cleaned_data['user']
-                user = User.objects.filter(username__iexact=username)
-                if user.count() == 0:
-                    # See if a secondary login was being used
-                    email = Email.objects.filter(address=username, active=True)
-                    # The error messages don't talk about 'email', as the field we're
-                    # looking at is still the 'username' field.
-                    if email.count() == 0:
-                        return err(400, "No such user: %s" % username)
-                    elif email.count() > 1:
-                        return err(500, "Multiple matching accounts for %s" % username)
-                    email = email.first()
-                    if not hasattr(email, 'person'):
-                        return err(400, "No person matches %s" % username)
-                    person = email.person
-                    if not hasattr(person, 'user'):
-                        return err(400, "No user matches: %s" % username)
-                    user = person.user
-                elif user.count() > 1:
-                    return err(500, "Multiple matching accounts for %s" % username)
-                else:
-                    user = user.first()
-                if not hasattr(user, 'person'):
-                    return err(400, "No person with username %s" % username)
-
-                saved_files = save_files(form)
-                authors, abstract, file_name, file_size = get_draft_meta(form, saved_files)
-                for a in authors:
-                    if not a['email']:
-                        raise ValidationError("Missing email address for author %s" % a)
-
-                submission = get_submission(form)
-                fill_in_submission(form, submission, authors, abstract, file_size)
-                apply_checkers(submission, file_name)
-
-                create_submission_event(request, submission, desc="Uploaded submission via api_submit")
-
-                errors = validate_submission(submission)
-                if errors:
-                    raise ValidationError(errors)
-
-                # must do this after validate_submission() or data needed for check may be invalid
-                if check_submission_revision_consistency(submission):
-                    return err( 409, "Submission failed due to a document revision inconsistency error "
-                                     "in the database. Please contact the secretariat for assistance.")
-
-                errors = [ c.message for c in submission.checks.all() if c.passed==False ]
-                if errors:
-                    raise ValidationError(errors)
-
-                if not username.lower() in [ a['email'].lower() for a in authors ]:
-                    raise ValidationError('Submitter %s is not one of the document authors' % user.username)
-
-                submission.submitter = user.person.formatted_email()
-                sent_to = accept_submission(submission, request)
-
-                return HttpResponse(
-                    "Upload of %s OK, confirmation requests sent to:\n  %s" % (submission.name, ',\n  '.join(sent_to)),
-                    content_type="text/plain")
-            else:
-                raise ValidationError(form.errors)
-        except IOError as e:
-            exception = e
-            return err(500, "IO Error: %s" % str(e))
-        except ValidationError as e:
-            exception = e
-            return err(400, "Validation Error: %s" % str(e))
-        except Exception as e:
-            exception = e
-            raise
-            return err(500, "Exception: %s" % str(e))
-        finally:
-            if exception and submission:
-                remove_submission_files(submission)
-                submission.delete()
-    else:
-        return err(405, "Method not allowed")
 
 def tool_instructions(request):
     return render(request, 'submit/tool_instructions.html', {'selected': 'instructions'})
diff --git a/ietf/templates/api/index.html b/ietf/templates/api/index.html
index 8373a387f..e21a50101 100644
--- a/ietf/templates/api/index.html
+++ b/ietf/templates/api/index.html
@@ -9,7 +9,7 @@
         <p>
             This section describes the autogenerated read-only API towards the database tables.  See also
             the
-            <a href="{% url 'ietf.submit.views.api_submit' %}">Internet-Draft submission API description</a>
+            <a href="{% url 'ietf.submit.views.api_submission' %}">Internet-Draft submission API description</a>
             and the
             <a href="#iesg-position-api">IESG ballot position API description</a>
         </p>
diff --git a/ietf/templates/submit/api_submit_info.html b/ietf/templates/submit/api_submit_info.html
index cd0d52410..75fc1abfc 100644
--- a/ietf/templates/submit/api_submit_info.html
+++ b/ietf/templates/submit/api_submit_info.html
@@ -1,56 +1,13 @@
 {% extends "base.html" %}
-{# Copyright The IETF Trust 2015-2022, All Rights Reserved #}
+{# Copyright The IETF Trust 2015-2024, All Rights Reserved #}
 {% load origin ietf_filters %}
-{% block title %}I-D submission API instructions{% endblock %}
+{% block title %}Obsolete I-D submission API notice{% endblock %}
 {% block content %}
     {% origin %}
-    <h1 class="mb-3">Internet-Draft submission API instructions</h1>
+    <h1 class="mb-3">Obsolete Internet-Draft submission API notice</h1>
     <p>
-        Note: API endpoint described here is known to have a slow response time or to fail
-        due to timeout for some Internet-Draft submissions, particularly those with large file sizes.
-        It is recommended to use the <a href="{% url 'ietf.submit.views.api_submission' %}">new API endpoint</a>
-        instead for increased reliability.
+        The API endpoint previously available here is obsolete and is no longer supported.
+        Please use the <a href="{% url 'ietf.submit.views.api_submission' %}">new API endpoint</a>
+        instead.
     </p>
-    <p>
-        A simplified Internet-Draft submission interface, intended for automation,
-        is available at <code>{% absurl 'ietf.submit.views.api_submit' %}</code>.
-    </p>
-    <p>
-        The interface accepts only XML uploads that can be processed on the server, and
-        requires the user to have a datatracker account.  A successful submit still requires
-        the same email confirmation round-trip as submissions done through the regular
-        <a href="{% url 'ietf.submit.views.upload_submission' %}">submission tool</a>.
-    </p>
-    <p>
-        This interface does not provide all the options which the regular submission tool does.
-        Some limitations:
-    </p>
-    <ul>
-        <li>Only XML-only uploads are supported, not text or combined.</li>
-        <li>Document replacement information cannot be supplied.</li>
-        <li>
-            The server expects <code>multipart/form-data</code>, supported by <code>curl</code> but <b>not</b> by <code>wget</code>.
-        </li>
-    </ul>
-    <p>
-        It takes two parameters:
-    </p>
-    <ul>
-        <li>
-            <code>user</code> which is the user login
-        </li>
-        <li>
-            <code>xml</code>, which is the submitted file
-        </li>
-    </ul>
-    <p>
-        It returns an appropriate http result code, and a brief explanatory text message.
-    </p>
-    <p>
-        Here is an example:
-    </p>
-    <pre class="border p-3">
-$ curl -S -F "user=user.name@example.com" -F "xml=@~/draft-user-example.xml" {% absurl 'ietf.submit.views.api_submit' %}
-Upload of draft-user-example OK, confirmation requests sent to:
-User Name &lt;user.name@example.com&gt;</pre>
-{% endblock %}
\ No newline at end of file
+{% endblock %}